Skip to content

Commit

Permalink
add Temporal.io related code encapsulation
Browse files Browse the repository at this point in the history
  • Loading branch information
krayevidi committed May 30, 2024
1 parent 98b8f5a commit 624711c
Show file tree
Hide file tree
Showing 21 changed files with 129 additions and 18 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.0.0 (2024-05-16)

* Initial release

## 1.1.0 (2024-05-30)

* add Temporal.io related code encapsulation
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ from temporalio.worker import WorkerConfig

DJANGO_TEMPORALIO = {
"URL": "localhost:7233",
"BASE_MODULE": "path.to.module",
"WORKER_CONFIGS": {
"main": WorkerConfig(
task_queue="MAIN_TASK_QUEUE",
Expand All @@ -45,12 +46,18 @@ DJANGO_TEMPORALIO = {

## Usage

Activities, workflows and schedules should be placed inside the base module defined by the `BASE_MODULE` setting,
preferably outside of any Django application, in order to keep the uses of
the [imports_passed_through](https://python.temporal.io/temporalio.workflow.unsafe.html) context manager encapsulated
inside the module, along with Temporal.io related code.

### Workflow and Activity Registry

The registry is a singleton that holds mappings between queue names and registered activities and workflows.
You can register activities and workflows using the `register` method.

Activities and workflows should be declared in `workflows.py` and `activities.py` modules respectively.
Activities and workflows should be declared in modules matching the following patterns `*workflows*.py` and
`*activities*.py` respectively.

```python
from temporalio import activity, workflow
Expand Down Expand Up @@ -123,3 +130,4 @@ DJANGO_TEMPORALIO: A dictionary containing the following keys:
- NAMESPACE: The Temporal.io namespace to use, defaults to `default`
- WORKER_CONFIGS: A dictionary containing worker configurations.
The key is the worker name and the value is a `WorkerConfig` instance.
- BASE_MODULE: A python module that holds workflows, activities and schedules, defaults to `None`
Empty file added dev/temporalio/__init__.py
Empty file.
2 changes: 1 addition & 1 deletion dev/activities.py → dev/temporalio/activities.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from temporalio import activity

from dev.temporalio import TestTaskQueues
from dev.temporalio.queues import TestTaskQueues
from django_temporalio.registry import queue_activities


Expand Down
Empty file added dev/temporalio/foo/__init__.py
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
File renamed without changes.
2 changes: 1 addition & 1 deletion dev/schedules.py → dev/temporalio/schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
ScheduleSpec,
)

from dev.temporalio import TestTaskQueues
from dev.temporalio.queues import TestTaskQueues
from django_temporalio.registry import schedules

schedules.register(
Expand Down
2 changes: 1 addition & 1 deletion dev/workflows.py → dev/temporalio/workflows.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from temporalio import workflow

from dev.temporalio import TestTaskQueues
from dev.temporalio.queues import TestTaskQueues
from django_temporalio.registry import queue_workflows


Expand Down
11 changes: 7 additions & 4 deletions dev/tests/registry/test_queue_activities.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from unittest import TestCase, mock

from django.utils.module_loading import autodiscover_modules
from django.test import override_settings
from temporalio import activity

from dev.temporalio import TestTaskQueues
from dev.temporalio.queues import TestTaskQueues
from django_temporalio.conf import SETTINGS_KEY
from django_temporalio.registry import queue_activities
from django_temporalio.utils import autodiscover_modules


@activity.defn
Expand All @@ -20,6 +22,7 @@ class QueueActivityRegistryTestCase(TestCase):
def tearDown(self):
queue_activities.clear_registry()

@override_settings(**{SETTINGS_KEY: {"BASE_MODULE": "dev.temporalio"}})
@mock.patch(
"django_temporalio.registry.autodiscover_modules", wraps=autodiscover_modules
)
Expand All @@ -34,14 +37,14 @@ def test_get_registry(self, mock_register, mock_autodiscover_modules):
registry = queue_activities.get_registry()

mock_register.assert_called_once_with(TestTaskQueues.MAIN)
mock_autodiscover_modules.assert_called_once_with("activities")
mock_autodiscover_modules.assert_called_once_with("*activities*")
self.assertEqual(len(registry), 1)
self.assertIn(TestTaskQueues.MAIN, registry)
activities = registry[TestTaskQueues.MAIN]
self.assertEqual(len(activities), 1)
self.assertEqual(
f"{activities[0].__module__}.{activities[0].__name__}",
"dev.activities.test_activity",
"dev.temporalio.activities.test_activity",
)

@mock.patch("django_temporalio.registry.autodiscover_modules")
Expand Down
9 changes: 6 additions & 3 deletions dev/tests/registry/test_queue_workflows.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from unittest import TestCase, mock

from django.test import override_settings
from temporalio import workflow

from dev.temporalio import TestTaskQueues
from dev.temporalio.queues import TestTaskQueues
from django_temporalio.conf import SETTINGS_KEY
from django_temporalio.registry import queue_workflows, autodiscover_modules


Expand All @@ -21,6 +23,7 @@ class QueueWorkflowRegistryTestCase(TestCase):
def tearDown(self):
queue_workflows.clear_registry()

@override_settings(**{SETTINGS_KEY: {"BASE_MODULE": "dev.temporalio"}})
@mock.patch(
"django_temporalio.registry.autodiscover_modules", wraps=autodiscover_modules
)
Expand All @@ -35,13 +38,13 @@ def test_get_registry(self, mock_register, mock_autodiscover_modules):
registry = queue_workflows.get_registry()

mock_register.assert_called_once_with(TestTaskQueues.MAIN)
mock_autodiscover_modules.assert_called_once_with("workflows")
mock_autodiscover_modules.assert_called_once_with("*workflows*")
self.assertEqual(len(registry), 1)
self.assertIn(TestTaskQueues.MAIN, registry)
workflows = registry[TestTaskQueues.MAIN]
self.assertEqual(len(workflows), 1)
self.assertEqual(
"dev.workflows.TestWorkflow",
"dev.temporalio.workflows.TestWorkflow",
f"{workflows[0].__module__}.{workflows[0].__name__}",
)

Expand Down
9 changes: 6 additions & 3 deletions dev/tests/registry/test_schedules.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from unittest import TestCase, mock

from django.utils.module_loading import autodiscover_modules
from django.test import override_settings
from temporalio.client import (
ScheduleActionStartWorkflow,
Schedule,
Expand All @@ -9,8 +9,10 @@
ScheduleRange,
)

from dev.temporalio import TestTaskQueues
from dev.temporalio.queues import TestTaskQueues
from django_temporalio.conf import SETTINGS_KEY
from django_temporalio.registry import schedules
from django_temporalio.utils import autodiscover_modules


class ScheduleRegistryTestCase(TestCase):
Expand Down Expand Up @@ -41,6 +43,7 @@ def setUpClass(cls):
def tearDown(self):
schedules.clear_registry()

@override_settings(**{SETTINGS_KEY: {"BASE_MODULE": "dev.temporalio"}})
@mock.patch(
"django_temporalio.registry.autodiscover_modules", wraps=autodiscover_modules
)
Expand All @@ -52,7 +55,7 @@ def test_get_registry(self, mock_register, mock_autodiscover_modules):
registry = schedules.get_registry()

mock_register.assert_called_once()
mock_autodiscover_modules.assert_called_once_with("schedules")
mock_autodiscover_modules.assert_called_once_with("*schedules*")
self.assertEqual(len(registry), 1)
self.assertIn("do-cool-stuff-every-hour", registry)

Expand Down
6 changes: 6 additions & 0 deletions dev/tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,24 @@ def test_default_settings(self):
self.assertEqual(temporalio_settings.URL, DEFAULTS["URL"])
self.assertEqual(temporalio_settings.NAMESPACE, DEFAULTS["NAMESPACE"])
self.assertEqual(temporalio_settings.WORKER_CONFIGS, DEFAULTS["WORKER_CONFIGS"])
self.assertEqual(temporalio_settings.BASE_MODULE, DEFAULTS["BASE_MODULE"])

def test_user_settings(self):
user_settings = {
"URL": "http://temporal:7233",
"NAMESPACE": "main",
"WORKER_CONFIGS": {"main": "config"},
"BASE_MODULE": "dev.temporalio",
}
with override_settings(**{SETTINGS_KEY: user_settings}):
self.assertEqual(temporalio_settings.URL, user_settings["URL"])
self.assertEqual(temporalio_settings.NAMESPACE, user_settings["NAMESPACE"])
self.assertEqual(
temporalio_settings.WORKER_CONFIGS, user_settings["WORKER_CONFIGS"]
)
self.assertEqual(
temporalio_settings.BASE_MODULE, user_settings["BASE_MODULE"]
)

def test_fallback_to_defaults(self):
user_settings = {
Expand All @@ -44,6 +49,7 @@ def test_fallback_to_defaults(self):
self.assertEqual(
temporalio_settings.WORKER_CONFIGS, DEFAULTS["WORKER_CONFIGS"]
)
self.assertEqual(temporalio_settings.BASE_MODULE, DEFAULTS["BASE_MODULE"])

def test_invalid_setting(self):
with self.assertRaises(AttributeError):
Expand Down
Empty file added dev/tests/utils/__init__.py
Empty file.
32 changes: 32 additions & 0 deletions dev/tests/utils/test_autodiscover_modules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from importlib import import_module
from unittest import TestCase, mock

from django.test import override_settings

from django_temporalio.conf import settings, SETTINGS_KEY, SettingIsNotSetError
from django_temporalio.utils import autodiscover_modules


class AutodiscoverModulesTestCase(TestCase):
"""
Test case for utils.autodiscover_modules.
"""

@override_settings(**{SETTINGS_KEY: {"BASE_MODULE": "dev.temporalio"}})
@mock.patch("django_temporalio.utils.import_module", wraps=import_module)
def test_autodiscover_modules(self, import_module_mock):
autodiscover_modules("*workflows*")

import_module_mock.assert_has_calls(
[
mock.call("dev.temporalio"),
mock.call("dev.temporalio.workflows"),
mock.call("dev.temporalio.foo.foo_workflows"),
mock.call("dev.temporalio.foo.bar.workflows_bar"),
]
)

def test_autodiscover_modules_raises_exception(self):
self.assertIsNone(settings.BASE_MODULE)
with self.assertRaises(SettingIsNotSetError):
autodiscover_modules("*workflows*")
5 changes: 5 additions & 0 deletions django_temporalio/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@
"URL": "http://localhost:7233",
"NAMESPACE": "default",
"WORKER_CONFIGS": {},
"BASE_MODULE": None,
}


class SettingIsNotSetError(Exception):
pass


class Settings:
def __init__(self):
self.defaults = DEFAULTS
Expand Down
9 changes: 5 additions & 4 deletions django_temporalio/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
from functools import wraps
from typing import Callable, Sequence, Type

from django.utils.module_loading import autodiscover_modules
from temporalio.client import Schedule

from django_temporalio.utils import autodiscover_modules


class ScheduleRegistry:
_registry: dict[str, Schedule]
Expand All @@ -26,7 +27,7 @@ def register(self, schedule_id: str, schedule: Schedule):
self._registry[schedule_id] = schedule

def get_registry(self):
autodiscover_modules("schedules")
autodiscover_modules("*schedules*")
return self._registry

def clear_registry(self):
Expand Down Expand Up @@ -85,8 +86,8 @@ def get_registry(self):


schedules = ScheduleRegistry()
queue_workflows = QueueRegistry("workflows", "__temporal_workflow_definition")
queue_activities = QueueRegistry("activities", "__temporal_activity_definition")
queue_workflows = QueueRegistry("*workflows*", "__temporal_workflow_definition")
queue_activities = QueueRegistry("*activities*", "__temporal_activity_definition")


@dataclass
Expand Down
46 changes: 46 additions & 0 deletions django_temporalio/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import fnmatch
import os
from importlib import import_module

from django_temporalio.conf import settings, SettingIsNotSetError


def autodiscover_modules(related_name_pattern: str):
"""
Autodiscover modules matching the related name pattern in the base module.
Example for the following directory structure:
foo/
workflows.py
activities.py
bar/
bar_workflows.py
activities.py
baz/
workflows_baz.py
activities.py
Calling `autodiscover_modules('foo', '*workflows*')` will discover the following modules:
- foo.workflows
- foo.bar.bar_workflows
- foo.bar.baz.workflows_baz
"""
base_module_name = settings.BASE_MODULE

if not base_module_name:
raise SettingIsNotSetError("BASE_MODULE setting must be set.")

base_module = import_module(base_module_name)
base_module_path = base_module.__path__[0]

for root, _, files in os.walk(base_module_path):
for file in files:
if not fnmatch.fnmatch(file, f"{related_name_pattern}.py"):
continue

module_name = root.replace(base_module_path, base_module_name).replace(
os.sep, "."
)
# import pdb; pdb.set_trace()
import_module(f"{module_name}.{file[:-3]}")

0 comments on commit 624711c

Please sign in to comment.