biased 0.1.2.dev13__tar.gz → 0.2.0.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/PKG-INFO +12 -9
- biased-0.2.0.dev1/pyproject.toml +76 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/admin.py +6 -6
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/aws_waf/aws_waf_regex_pattern.py +2 -2
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/aws_waf/aws_waf_whitelist_middleware.py +2 -2
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/aws_waf/url_patterns.py +8 -7
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/filters/entity_filter.py +3 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/filters/input_filter.py +17 -12
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/models/created_at_modified_at_mixin.py +2 -2
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/models/deleted_at_mixin.py +2 -2
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/utils/choices.py +1 -1
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/utils/get_client_ip.py +1 -1
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django_jsonform/jsonform_action.py +1 -6
- biased-0.2.0.dev1/src/biased/scripts/dotenv_run.py +45 -0
- biased-0.2.0.dev1/src/biased/structlog/__init__.py +0 -0
- biased-0.2.0.dev1/src/biased/temporal/__init__.py +0 -0
- biased-0.2.0.dev1/src/biased/temporal/activities/__init__.py +0 -0
- biased-0.2.0.dev1/src/biased/temporal/activities/consts.py +11 -0
- biased-0.2.0.dev1/src/biased/temporal/activities/dtos.py +11 -0
- biased-0.2.0.dev1/src/biased/temporal/activities/echo.py +12 -0
- biased-0.2.0.dev1/src/biased/temporal/activities/types.py +16 -0
- biased-0.2.0.dev1/src/biased/temporal/activities/utils.py +50 -0
- biased-0.2.0.dev1/src/biased/temporal/django/__init__.py +0 -0
- biased-0.2.0.dev1/src/biased/temporal/django/admin/__init__.py +0 -0
- biased-0.2.0.dev1/src/biased/temporal/django/admin/trigger_temporal_workflow_mixin.py +65 -0
- biased-0.2.0.dev1/src/biased/temporal/django/temporal_ui.py +11 -0
- biased-0.2.0.dev1/src/biased/temporal/dtos.py +25 -0
- biased-0.2.0.dev1/src/biased/temporal/services/__init__.py +0 -0
- biased-0.2.0.dev1/src/biased/temporal/services/temporal_ui.py +21 -0
- biased-0.2.0.dev1/src/biased/temporal/temporal_client_holder.py +25 -0
- biased-0.2.0.dev1/src/biased/temporal/workflows/__init__.py +0 -0
- biased-0.2.0.dev1/src/biased/temporal/workflows/base.py +61 -0
- biased-0.2.0.dev1/src/biased/temporal/workflows/dtos.py +11 -0
- biased-0.2.0.dev1/src/biased/temporal/workflows/echo.py +28 -0
- biased-0.2.0.dev1/src/biased/temporal/workflows/utils.py +7 -0
- biased-0.2.0.dev1/src/biased/temporal/workflows/workflow.py +73 -0
- biased-0.2.0.dev1/src/biased/utils/__init__.py +0 -0
- biased-0.1.2.dev13/pyproject.toml +0 -40
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/README.md +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/__init__.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/consts.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/__init__.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/apps.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/aws_waf/__init__.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/dtos.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/filters/__init__.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/forms/__init__.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/forms/json_editor_widget.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/forms/pydantic_model_form_field.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/forms/ulid_form_field.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/models/__init__.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/models/bank_account.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/models/char_field.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/models/default_money_amount_field.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/models/multi_select_field.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/models/pydantic_model_field.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/templates/actions_change_form.html +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/templates/admin/input_filter.html +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/utils/__init__.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/utils/get_user_agent.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/utils/reverse.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/validators/__init__.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/validators/pydantic.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django_jsonform/__init__.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django_jsonform/apps.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django_jsonform/templates/admin_form_action/django_jsonform_form.html +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django_redis/__init__.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django_redis/pydantic_serializer.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/dtos/__init__.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/dtos/base.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/dtos/env_file_paths.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/dtos/logging.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/logging/__init__.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/logging/dict_config.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/logging/settings.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/logging/utils.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/ninja/__init__.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/ninja/json_renderrer.py +0 -0
- /biased-0.1.2.dev13/src/biased/pydantic/__init__.py → /biased-0.2.0.dev1/src/biased/py.typed +0 -0
- {biased-0.1.2.dev13/src/biased/structlog → biased-0.2.0.dev1/src/biased/pydantic}/__init__.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/pydantic/base64_pydantic_model.py +0 -0
- {biased-0.1.2.dev13/src/biased/utils → biased-0.2.0.dev1/src/biased/scripts}/__init__.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/structlog/processors.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/structlog/structlog_json_encoder.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/types.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/utils/aba_routing_number.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/utils/attrgetter_default.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/utils/calculate_age.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/utils/default_json_encoder.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/utils/filter_not_none_values.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/utils/time.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/utils/ulid.py +0 -0
- {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/utils/uuid.py +0 -0
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: biased
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0.dev1
|
|
4
4
|
Summary: Various reusable components, helpers, and utilities which I would like to use in any Python project
|
|
5
5
|
Author: Dmitry Tatarkin
|
|
6
6
|
Author-email: Dmitry Tatarkin <tatarkin@gmail.com>
|
|
7
|
-
Requires-Dist: injector
|
|
7
|
+
Requires-Dist: injector
|
|
8
8
|
Requires-Dist: pydantic>=2
|
|
9
|
-
Requires-Dist: python-dateutil
|
|
10
|
-
Requires-Dist: structlog
|
|
11
|
-
Requires-Dist: ulid-py
|
|
12
|
-
Requires-Dist:
|
|
13
|
-
Requires-Dist: django
|
|
14
|
-
Requires-Dist: django-
|
|
15
|
-
Requires-Dist: django-
|
|
9
|
+
Requires-Dist: python-dateutil
|
|
10
|
+
Requires-Dist: structlog
|
|
11
|
+
Requires-Dist: ulid-py
|
|
12
|
+
Requires-Dist: python-dotenv[cli]
|
|
13
|
+
Requires-Dist: django ; extra == 'django'
|
|
14
|
+
Requires-Dist: django-jsonform ; extra == 'django-jsonform'
|
|
15
|
+
Requires-Dist: django-redis ; extra == 'django-redis'
|
|
16
|
+
Requires-Dist: django-ninja ; extra == 'ninja'
|
|
17
|
+
Requires-Dist: temporalio ; extra == 'temporal'
|
|
16
18
|
Requires-Python: >=3.12
|
|
17
19
|
Provides-Extra: django
|
|
18
20
|
Provides-Extra: django-jsonform
|
|
19
21
|
Provides-Extra: django-redis
|
|
20
22
|
Provides-Extra: ninja
|
|
23
|
+
Provides-Extra: temporal
|
|
21
24
|
Description-Content-Type: text/markdown
|
|
22
25
|
|
|
23
26
|
# About
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "biased"
|
|
3
|
+
version = "0.2.0.dev1"
|
|
4
|
+
description = "Various reusable components, helpers, and utilities which I would like to use in any Python project"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Dmitry Tatarkin", email = "tatarkin@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"injector",
|
|
12
|
+
"pydantic>=2",
|
|
13
|
+
"python-dateutil",
|
|
14
|
+
"structlog",
|
|
15
|
+
"ulid-py",
|
|
16
|
+
"python-dotenv[cli]",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
dotenv-run = "biased.scripts.dotenv_run:main"
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
django = [
|
|
24
|
+
"django",
|
|
25
|
+
]
|
|
26
|
+
ninja = [
|
|
27
|
+
"django-ninja",
|
|
28
|
+
]
|
|
29
|
+
django-redis = [
|
|
30
|
+
"django-redis",
|
|
31
|
+
]
|
|
32
|
+
django-jsonform = [
|
|
33
|
+
"django-jsonform",
|
|
34
|
+
]
|
|
35
|
+
temporal = [
|
|
36
|
+
"temporalio",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["uv_build>=0.8.16,<0.9.0"]
|
|
41
|
+
build-backend = "uv_build"
|
|
42
|
+
|
|
43
|
+
[dependency-groups]
|
|
44
|
+
dev = [
|
|
45
|
+
"pytest",
|
|
46
|
+
"rust-just",
|
|
47
|
+
"python-dotenv",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
lint = [
|
|
51
|
+
"bandit",
|
|
52
|
+
"django-stubs[compatible-mypy]",
|
|
53
|
+
"mypy",
|
|
54
|
+
"pre-commit",
|
|
55
|
+
"pyright",
|
|
56
|
+
"ruff",
|
|
57
|
+
"types-python-dateutil",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
[tool.bandit.assert_used]
|
|
62
|
+
skips = ["**/test_*.py"]
|
|
63
|
+
|
|
64
|
+
[tool.mypy]
|
|
65
|
+
packages = ["biased"]
|
|
66
|
+
|
|
67
|
+
[[tool.mypy.overrides]]
|
|
68
|
+
module = [
|
|
69
|
+
"multiselectfield.*",
|
|
70
|
+
"jsoneditor.*",
|
|
71
|
+
"pydantic_settings.*",
|
|
72
|
+
"django_redis.*",
|
|
73
|
+
"ninja.*",
|
|
74
|
+
"admin_form_action.*",
|
|
75
|
+
]
|
|
76
|
+
ignore_missing_imports = true
|
|
@@ -45,7 +45,7 @@ def admin_change_link(short_description: str, empty_description: str = "-"):
|
|
|
45
45
|
return empty_description
|
|
46
46
|
return admin_change_html_link(instance=related_obj)
|
|
47
47
|
|
|
48
|
-
field_func.short_description = short_description
|
|
48
|
+
field_func.short_description = short_description # type: ignore[attr-defined]
|
|
49
49
|
return field_func
|
|
50
50
|
|
|
51
51
|
return wrapper
|
|
@@ -63,7 +63,7 @@ def id_admin_change_link(
|
|
|
63
63
|
app_label=app_label, model_name=model_name, object_id=object_id, id_slug=id_slug
|
|
64
64
|
)
|
|
65
65
|
|
|
66
|
-
field_func.short_description = short_description
|
|
66
|
+
field_func.short_description = short_description # type: ignore[attr-defined]
|
|
67
67
|
return field_func
|
|
68
68
|
|
|
69
69
|
return wrapper
|
|
@@ -93,7 +93,7 @@ def admin_query_params_list_link(
|
|
|
93
93
|
model_type=model_type, title=title, query_kwargs=query_params, target=target
|
|
94
94
|
)
|
|
95
95
|
|
|
96
|
-
field_func.short_description = short_description
|
|
96
|
+
field_func.short_description = short_description # type: ignore[attr-defined]
|
|
97
97
|
return field_func
|
|
98
98
|
|
|
99
99
|
return wrapper
|
|
@@ -103,7 +103,7 @@ def _build_html_image_tag(url: str, attributes: dict[str, Any] | None = None) ->
|
|
|
103
103
|
if attributes:
|
|
104
104
|
attributes_html = mark_safe(" ".join(f'{attr}="{value}"' for attr, value in attributes.items())) # nosec B308:blacklist, B703:django_mark_safe
|
|
105
105
|
else:
|
|
106
|
-
attributes_html = ""
|
|
106
|
+
attributes_html = mark_safe("") # nosec B308:blacklist, B703:django_mark_safe
|
|
107
107
|
return format_html('<img src="{url}" {attributes_html}/>', url=url, attributes_html=attributes_html)
|
|
108
108
|
|
|
109
109
|
|
|
@@ -115,7 +115,7 @@ def html_image_tag(short_description: str, attributes: dict[str, Any] | None = N
|
|
|
115
115
|
return empty_description
|
|
116
116
|
return _build_html_image_tag(url=image_url, attributes=attributes)
|
|
117
117
|
|
|
118
|
-
field_func.short_description = short_description
|
|
118
|
+
field_func.short_description = short_description # type: ignore[attr-defined]
|
|
119
119
|
return field_func
|
|
120
120
|
|
|
121
121
|
return wrapper
|
|
@@ -132,7 +132,7 @@ def url_link(short_description: str, content: str = "-", target: str = "_self"):
|
|
|
132
132
|
params.update(func(self, obj))
|
|
133
133
|
return format_html('<a href="{url}" target="{target}">{content}</a>', **params)
|
|
134
134
|
|
|
135
|
-
field_func.short_description = short_description
|
|
135
|
+
field_func.short_description = short_description # type: ignore[attr-defined]
|
|
136
136
|
return field_func
|
|
137
137
|
|
|
138
138
|
return wrapper
|
|
@@ -36,9 +36,9 @@ def validate_aws_waf_regex_patterns(
|
|
|
36
36
|
|
|
37
37
|
for regex_pattern, url in iter_regex_patterns(url_patterns):
|
|
38
38
|
for url_template, url_params in normalize(regex_pattern):
|
|
39
|
-
|
|
39
|
+
url_params_set = set(url_params)
|
|
40
40
|
with context(url=url):
|
|
41
|
-
remaining_params =
|
|
41
|
+
remaining_params = url_params_set - context.keys()
|
|
42
42
|
if not remaining_params:
|
|
43
43
|
url_example = "/" + url_template % context
|
|
44
44
|
if search_aws_waf_pattern(url_example=url_example) is None:
|
{biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/aws_waf/aws_waf_whitelist_middleware.py
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from collections.abc import
|
|
2
|
+
from collections.abc import Sequence
|
|
3
3
|
|
|
4
4
|
from asgiref.sync import iscoroutinefunction, markcoroutinefunction
|
|
5
5
|
from django.conf import settings
|
|
@@ -9,7 +9,7 @@ from django.http import HttpRequest, HttpResponse
|
|
|
9
9
|
from biased.django.aws_waf.aws_waf_regex_pattern import AwsWafRegexPatternSet
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
class AwsWafWhitelistMiddleware(ABC
|
|
12
|
+
class AwsWafWhitelistMiddleware(ABC):
|
|
13
13
|
async_capable = True
|
|
14
14
|
sync_capable = False
|
|
15
15
|
aws_waf_regex_pattern_sets: Sequence[AwsWafRegexPatternSet] = tuple()
|
|
@@ -2,11 +2,11 @@ from collections.abc import Iterable, Sequence
|
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
4
|
from django.urls import URLPattern, URLResolver
|
|
5
|
-
from django.urls.resolvers import RoutePattern
|
|
5
|
+
from django.urls.resolvers import RegexPattern, RoutePattern
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class UrlArgsContext:
|
|
9
|
-
def __init__(self, *args: Sequence[
|
|
9
|
+
def __init__(self, *args: tuple[Sequence[str], dict[str, Any]]) -> None:
|
|
10
10
|
self._urls: list[str] = [""]
|
|
11
11
|
self._args = args
|
|
12
12
|
|
|
@@ -28,7 +28,7 @@ class UrlArgsContext:
|
|
|
28
28
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
29
29
|
self._urls.pop()
|
|
30
30
|
|
|
31
|
-
def keys(self):
|
|
31
|
+
def keys(self) -> set[str]:
|
|
32
32
|
keys: set[str] = set()
|
|
33
33
|
for prefixes, params in self._args:
|
|
34
34
|
for prefix in prefixes:
|
|
@@ -53,9 +53,10 @@ def _iter_url_patterns(
|
|
|
53
53
|
raise RuntimeError(f"Unexpected url_pattern type: {type(url_pattern)}")
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def _iter_route_patterns(url_patterns: Iterable[RoutePattern | URLPattern]) -> Iterable[
|
|
56
|
+
def _iter_route_patterns(url_patterns: Iterable[RoutePattern | URLPattern]) -> Iterable[RoutePattern | RegexPattern]:
|
|
57
57
|
for url_pattern in url_patterns:
|
|
58
58
|
if isinstance(url_pattern, URLPattern):
|
|
59
|
+
assert isinstance(url_pattern.pattern, (RoutePattern | RegexPattern)) # nosec B101
|
|
59
60
|
yield url_pattern.pattern
|
|
60
61
|
elif isinstance(url_pattern, RoutePattern):
|
|
61
62
|
yield url_pattern
|
|
@@ -63,15 +64,15 @@ def _iter_route_patterns(url_patterns: Iterable[RoutePattern | URLPattern]) -> I
|
|
|
63
64
|
raise RuntimeError(f"Unexpected url_pattern type: {type(url_pattern)}")
|
|
64
65
|
|
|
65
66
|
|
|
66
|
-
def _route_pattern_to_regex_pattern(route_pattern: RoutePattern) -> str:
|
|
67
|
+
def _route_pattern_to_regex_pattern(route_pattern: RoutePattern | RegexPattern) -> str:
|
|
67
68
|
return route_pattern.regex.pattern.removeprefix("^").replace("/", r"\/")
|
|
68
69
|
|
|
69
70
|
|
|
70
|
-
def _route_patterns_to_regex_pattern(route_patterns: Iterable[RoutePattern]) -> str:
|
|
71
|
+
def _route_patterns_to_regex_pattern(route_patterns: Iterable[RoutePattern | RegexPattern]) -> str:
|
|
71
72
|
return "^" + "".join(map(_route_pattern_to_regex_pattern, route_patterns))
|
|
72
73
|
|
|
73
74
|
|
|
74
|
-
def _route_patterns_to_str(route_patterns: Iterable[RoutePattern]) -> str:
|
|
75
|
+
def _route_patterns_to_str(route_patterns: Iterable[RoutePattern | RegexPattern]) -> str:
|
|
75
76
|
return "".join(map(str, route_patterns))
|
|
76
77
|
|
|
77
78
|
|
|
@@ -40,9 +40,12 @@ class EntityFilter(CommaSeparatedInputFilter):
|
|
|
40
40
|
def value_to_filter(self, value: str) -> Q:
|
|
41
41
|
value_type, parsed_value = _parse_value(value=value)
|
|
42
42
|
if value_type == ValueType.int:
|
|
43
|
+
assert isinstance(parsed_value, int) # nosec B101
|
|
43
44
|
return self.int_value_to_filter(value=parsed_value)
|
|
44
45
|
if value_type == ValueType.ulid:
|
|
46
|
+
assert isinstance(parsed_value, str) # nosec B101
|
|
45
47
|
return self.ulid_value_to_filter(value=parsed_value)
|
|
46
48
|
if value_type == ValueType.str:
|
|
49
|
+
assert isinstance(parsed_value, str) # nosec B101
|
|
47
50
|
return self.str_value_to_filter(value=parsed_value)
|
|
48
51
|
raise NotImplementedError(f"{value_type} is not supported")
|
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
import operator
|
|
2
2
|
from abc import ABC, abstractmethod
|
|
3
|
+
from collections.abc import Iterator
|
|
3
4
|
from functools import reduce
|
|
4
5
|
|
|
5
|
-
from django.contrib.admin import SimpleListFilter
|
|
6
|
-
from django.
|
|
6
|
+
from django.contrib.admin import ModelAdmin, SimpleListFilter
|
|
7
|
+
from django.contrib.admin.views.main import ChangeList
|
|
8
|
+
from django.db.models import Q, QuerySet
|
|
9
|
+
from django.http import HttpRequest
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
class InputFilter(SimpleListFilter):
|
|
10
13
|
template = "admin/input_filter.html"
|
|
11
14
|
|
|
12
|
-
def lookups(self, request, model_admin):
|
|
15
|
+
def lookups(self, request: HttpRequest, model_admin: ModelAdmin) -> tuple[tuple[()], ...]: # type: ignore[override]
|
|
13
16
|
# Dummy, required to show the filter.
|
|
14
17
|
return ((),)
|
|
15
18
|
|
|
16
|
-
def get_facet_counts(self, pk_attname, filtered_qs):
|
|
19
|
+
def get_facet_counts(self, pk_attname: str, filtered_qs: QuerySet) -> dict:
|
|
17
20
|
return {}
|
|
18
21
|
|
|
19
|
-
def choices(self, changelist):
|
|
22
|
+
def choices(self, changelist: ChangeList) -> Iterator:
|
|
20
23
|
# Grab only the "all" option.
|
|
21
24
|
all_choice = next(super().choices(changelist))
|
|
22
|
-
all_choice["query_parts"] = (
|
|
25
|
+
all_choice["query_parts"] = ( # type: ignore[typeddict-unknown-key]
|
|
23
26
|
(k, v) for k, values in changelist.get_filters_params().items() for v in values if k != self.parameter_name
|
|
24
27
|
)
|
|
25
28
|
yield all_choice
|
|
@@ -30,10 +33,11 @@ class CommaSeparatedInputFilter(InputFilter, ABC):
|
|
|
30
33
|
def value_to_filter(self, value: str) -> Q:
|
|
31
34
|
pass
|
|
32
35
|
|
|
33
|
-
def queryset(self, request, queryset):
|
|
34
|
-
|
|
36
|
+
def queryset(self, request: HttpRequest, queryset: QuerySet) -> QuerySet | None:
|
|
37
|
+
filter_value = self.value()
|
|
38
|
+
if filter_value is not None:
|
|
35
39
|
filters: list[Q] = []
|
|
36
|
-
for i in
|
|
40
|
+
for i in filter_value.split(","):
|
|
37
41
|
value = i.strip()
|
|
38
42
|
if not value:
|
|
39
43
|
continue
|
|
@@ -46,10 +50,11 @@ class CommaSeparatedInputFilter(InputFilter, ABC):
|
|
|
46
50
|
class StrArrayInputFilter(InputFilter):
|
|
47
51
|
query_name: str
|
|
48
52
|
|
|
49
|
-
def queryset(self, request, queryset):
|
|
50
|
-
|
|
53
|
+
def queryset(self, request: HttpRequest, queryset: QuerySet) -> QuerySet | None:
|
|
54
|
+
filter_value = self.value()
|
|
55
|
+
if filter_value is not None:
|
|
51
56
|
items: list[str] = []
|
|
52
|
-
for i in
|
|
57
|
+
for i in filter_value.split(","):
|
|
53
58
|
value = i.strip()
|
|
54
59
|
if not value:
|
|
55
60
|
continue
|
{biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/models/created_at_modified_at_mixin.py
RENAMED
|
@@ -2,8 +2,8 @@ from django.db import models
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class CreatedAtModifiedAtMixin(models.Model):
|
|
5
|
-
created_at = models.DateTimeField(auto_now_add=True, editable=False)
|
|
6
|
-
modified_at = models.DateTimeField(auto_now=True, editable=False)
|
|
5
|
+
created_at = models.DateTimeField(auto_now_add=True, editable=False) # type: ignore[var-annotated]
|
|
6
|
+
modified_at = models.DateTimeField(auto_now=True, editable=False) # type: ignore[var-annotated]
|
|
7
7
|
|
|
8
8
|
class Meta:
|
|
9
9
|
abstract = True
|
|
@@ -8,12 +8,12 @@ class DeletedAtQuerySet(models.QuerySet):
|
|
|
8
8
|
return self.filter(deleted_at__isnull=True)
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class DeletedAtManager(models.Manager.from_queryset(DeletedAtQuerySet)):
|
|
11
|
+
class DeletedAtManager(models.Manager.from_queryset(DeletedAtQuerySet)): # type: ignore[misc]
|
|
12
12
|
pass
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class DeletedAtMixin(models.Model):
|
|
16
|
-
deleted_at = models.DateTimeField(null=True, blank=True)
|
|
16
|
+
deleted_at = models.DateTimeField(null=True, blank=True) # type: ignore[var-annotated]
|
|
17
17
|
|
|
18
18
|
objects = DeletedAtManager()
|
|
19
19
|
|
|
@@ -10,4 +10,4 @@ def enum_to_choices(enum: type[Enum]) -> Iterable[tuple[str, str]]:
|
|
|
10
10
|
|
|
11
11
|
def choices_to_text_choices(enum_name: str, choices: Iterable[tuple[str, str]]) -> TextChoices:
|
|
12
12
|
attrs = {value: (value, label) for value, label in choices}
|
|
13
|
-
return TextChoices(enum_name, attrs)
|
|
13
|
+
return TextChoices(enum_name, attrs) # type: ignore[call-overload]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from django.http import HttpRequest
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
def get_client_ip(request: HttpRequest) -> str:
|
|
4
|
+
def get_client_ip(request: HttpRequest) -> str | None:
|
|
5
5
|
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
|
|
6
6
|
if x_forwarded_for:
|
|
7
7
|
ip = x_forwarded_for.split(",")[0]
|
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
from typing import TypeVar
|
|
2
|
-
|
|
3
1
|
from admin_form_action import Decorator, form_action
|
|
4
2
|
from django import forms
|
|
5
3
|
from django.contrib.admin import ModelAdmin
|
|
6
4
|
|
|
7
|
-
FormT = TypeVar("FormT", bound=forms.Form)
|
|
8
|
-
ModelAdminT = TypeVar("ModelAdminT", bound=ModelAdmin)
|
|
9
|
-
|
|
10
5
|
|
|
11
|
-
def jsonform_action[FormT: forms.Form](
|
|
6
|
+
def jsonform_action[FormT: forms.Form, ModelAdminT: ModelAdmin](
|
|
12
7
|
form_class: type[FormT],
|
|
13
8
|
*,
|
|
14
9
|
template: str | None = None,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
parser = argparse.ArgumentParser(
|
|
10
|
+
description="Load environment variables from files and run a command.",
|
|
11
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
12
|
+
epilog="Example:\n dotenv-run -f .env -f .env.local -- python manage.py runserver",
|
|
13
|
+
)
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
"-f",
|
|
16
|
+
"--file",
|
|
17
|
+
dest="files",
|
|
18
|
+
action="append",
|
|
19
|
+
default=[],
|
|
20
|
+
help="Path to the .env file to load. Can be specified multiple times.",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"command",
|
|
24
|
+
nargs=argparse.REMAINDER,
|
|
25
|
+
help="Command to execute, followed by its arguments.",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
args = parser.parse_args()
|
|
29
|
+
|
|
30
|
+
for f in args.files:
|
|
31
|
+
load_dotenv(f, override=True)
|
|
32
|
+
|
|
33
|
+
if args.command and args.command[0] == "--":
|
|
34
|
+
cmd_args = args.command[1:]
|
|
35
|
+
else:
|
|
36
|
+
cmd_args = args.command
|
|
37
|
+
|
|
38
|
+
if not cmd_args:
|
|
39
|
+
parser.error("No command specified to execute.")
|
|
40
|
+
|
|
41
|
+
os.execvp(cmd_args[0], cmd_args) # nosec B606:start_process_with_no_shell
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
|
|
3
|
+
from temporalio.common import RetryPolicy
|
|
4
|
+
|
|
5
|
+
DEFAULT_START_TO_CLOSE_TIMEOUT = timedelta(seconds=30)
|
|
6
|
+
|
|
7
|
+
DEFAULT_RETRY_POLICY = RetryPolicy(
|
|
8
|
+
initial_interval=timedelta(seconds=5),
|
|
9
|
+
backoff_coefficient=2,
|
|
10
|
+
maximum_interval=timedelta(days=1),
|
|
11
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
|
|
3
|
+
from temporalio import activity
|
|
4
|
+
|
|
5
|
+
from biased.temporal.activities.dtos import EchoActivityArg, EchoActivityResult
|
|
6
|
+
from biased.temporal.activities.utils import with_default_execute_params
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@activity.defn(name="echo")
|
|
10
|
+
@with_default_execute_params(start_to_close_timeout=timedelta(seconds=3))
|
|
11
|
+
async def echo_activity(arg: EchoActivityArg) -> EchoActivityResult:
|
|
12
|
+
return EchoActivityResult(message=arg.message)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from collections.abc import Awaitable
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from temporalio.common import RetryPolicy
|
|
6
|
+
|
|
7
|
+
from biased.dtos.base import BaseDto
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ActivityFuncWrapper[ArgT: BaseDto, ResultT: BaseDto](Protocol):
|
|
11
|
+
"""Protocol for activity functions decorated with with_default_execute_params."""
|
|
12
|
+
|
|
13
|
+
default_start_to_close_timeout: timedelta
|
|
14
|
+
default_retry_policy: RetryPolicy | None
|
|
15
|
+
|
|
16
|
+
def __call__(self, arg: ArgT, /) -> Awaitable[ResultT]: ...
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from collections.abc import Awaitable, Callable
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
from temporalio.common import RetryPolicy
|
|
6
|
+
|
|
7
|
+
from biased.dtos.base import BaseDto
|
|
8
|
+
from biased.temporal.activities.consts import DEFAULT_START_TO_CLOSE_TIMEOUT
|
|
9
|
+
from biased.temporal.activities.types import ActivityFuncWrapper
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def with_default_execute_params[ArgT: BaseDto, ResultT: BaseDto](
|
|
13
|
+
start_to_close_timeout: timedelta = DEFAULT_START_TO_CLOSE_TIMEOUT,
|
|
14
|
+
retry_policy: RetryPolicy | None = None,
|
|
15
|
+
) -> Callable[[Callable[[ArgT], Awaitable[ResultT]]], ActivityFuncWrapper[ArgT, ResultT]]:
|
|
16
|
+
"""Decorator to set default execution parameters on an activity function.
|
|
17
|
+
|
|
18
|
+
Sets `default_start_to_close_timeout` and optionally `default_retry_policy`
|
|
19
|
+
attributes on the activity function for use with `workflow.execute_activity()`.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
start_to_close_timeout: Default timeout for the activity (default: DEFAULT_START_TO_CLOSE_TIMEOUT).
|
|
23
|
+
retry_policy: Optional default retry policy for the activity.
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
@activity.defn(name="my_activity")
|
|
27
|
+
@with_default_execute_params(
|
|
28
|
+
start_to_close_timeout=timedelta(minutes=5),
|
|
29
|
+
retry_policy=RetryPolicy(
|
|
30
|
+
initial_interval=timedelta(seconds=1),
|
|
31
|
+
backoff_coefficient=2,
|
|
32
|
+
maximum_interval=timedelta(hours=1),
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
async def my_activity(arg: MyActivityArg) -> MyActivityResult:
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
# Then in workflow:
|
|
39
|
+
from biased.temporal.workflows.workflow import execute_activity
|
|
40
|
+
|
|
41
|
+
await execute_activity(my_activity, arg=MyActivityArg(...))
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def decorator(func: Callable[[ArgT], Awaitable[ResultT]]) -> ActivityFuncWrapper[ArgT, ResultT]:
|
|
45
|
+
func = cast(ActivityFuncWrapper[ArgT, ResultT], func)
|
|
46
|
+
func.default_start_to_close_timeout = start_to_close_timeout
|
|
47
|
+
func.default_retry_policy = retry_policy
|
|
48
|
+
return func
|
|
49
|
+
|
|
50
|
+
return decorator
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from django.contrib import messages
|
|
4
|
+
from django.contrib.admin import ModelAdmin
|
|
5
|
+
from django.http import HttpRequest
|
|
6
|
+
from django.utils.html import format_html
|
|
7
|
+
from temporalio.client import WorkflowHandle
|
|
8
|
+
|
|
9
|
+
from biased.dtos.base import BaseDto
|
|
10
|
+
from biased.temporal.django.temporal_ui import DjangoTemporalUi
|
|
11
|
+
from biased.temporal.temporal_client_holder import TemporalClientHolder
|
|
12
|
+
from biased.temporal.workflows.base import BaseWorkflow
|
|
13
|
+
from biased.temporal.workflows.workflow import start_workflow
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TriggerTemporalWorkflowMixin(ModelAdmin):
|
|
17
|
+
def _start_temporal_workflow[ArgT: BaseDto, ResultT: BaseDto](
|
|
18
|
+
self,
|
|
19
|
+
request: HttpRequest,
|
|
20
|
+
workflow: type[BaseWorkflow[ArgT, ResultT]],
|
|
21
|
+
arg: ArgT,
|
|
22
|
+
django_temporal_ui: DjangoTemporalUi,
|
|
23
|
+
*,
|
|
24
|
+
task_queue: str = "default",
|
|
25
|
+
context: Any = None,
|
|
26
|
+
) -> WorkflowHandle:
|
|
27
|
+
"""Start a Temporal workflow from Django admin.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
request: Django HTTP request (must have state["temporal_client_holder"])
|
|
31
|
+
workflow: Workflow class (must inherit from BaseWorkflow)
|
|
32
|
+
arg: Workflow argument DTO
|
|
33
|
+
task_queue: Task queue name
|
|
34
|
+
context: Optional context to display in the success message
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
WorkflowHandle for the started workflow.
|
|
38
|
+
"""
|
|
39
|
+
state: dict[str, Any] = getattr(request, "state", {})
|
|
40
|
+
temporal_client_holder: TemporalClientHolder = state["temporal_client_holder"]
|
|
41
|
+
|
|
42
|
+
handle: WorkflowHandle = start_workflow(
|
|
43
|
+
temporal_client_holder=temporal_client_holder,
|
|
44
|
+
workflow=workflow,
|
|
45
|
+
arg=arg,
|
|
46
|
+
task_queue=task_queue,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if context is None:
|
|
50
|
+
context_str = ""
|
|
51
|
+
else:
|
|
52
|
+
context_str = f", {context}"
|
|
53
|
+
self.message_user(
|
|
54
|
+
request,
|
|
55
|
+
message=format_html(
|
|
56
|
+
"Successfully started Temporal workflow {workflow_html_link}{context_str}",
|
|
57
|
+
workflow_html_link=django_temporal_ui.build_workflow_html_link(
|
|
58
|
+
workflow_id=handle.id, content=workflow.get_workflow_name()
|
|
59
|
+
),
|
|
60
|
+
context_str=context_str,
|
|
61
|
+
),
|
|
62
|
+
level=messages.SUCCESS,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return handle
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from django.utils.html import format_html
|
|
2
|
+
|
|
3
|
+
from biased.temporal.services.temporal_ui import TemporalUi
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DjangoTemporalUi(TemporalUi):
|
|
7
|
+
def build_workflow_html_link(self, workflow_id: str, content: str | None = None) -> str:
|
|
8
|
+
url = self.build_workflow_url(workflow_id=workflow_id)
|
|
9
|
+
if content is None:
|
|
10
|
+
content = workflow_id
|
|
11
|
+
return format_html('<a href="{}" target="_blank">{}</a>', str(url), content)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from pydantic import HttpUrl
|
|
2
|
+
from pydantic_settings import SettingsConfigDict
|
|
3
|
+
|
|
4
|
+
from biased.dtos.base import BaseDto
|
|
5
|
+
from biased.dtos.env_file_paths import EnvFilePathsSettings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TemporalClientParams(BaseDto):
|
|
9
|
+
target_host: str
|
|
10
|
+
namespace: str = "default"
|
|
11
|
+
api_key: str | None = None
|
|
12
|
+
identity: str | None = None
|
|
13
|
+
lazy: bool = True
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TemporalClientSettings(EnvFilePathsSettings, TemporalClientParams):
|
|
17
|
+
model_config = SettingsConfigDict(extra="ignore", env_prefix="TEMPORAL_CLIENT_")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TemporalParams(BaseDto):
|
|
21
|
+
ui_base_url: HttpUrl
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TemporalSettings(EnvFilePathsSettings, TemporalParams):
|
|
25
|
+
model_config = SettingsConfigDict(extra="ignore", env_prefix="TEMPORAL_", env_nested_delimiter="__")
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from injector import inject
|
|
2
|
+
from pydantic import HttpUrl
|
|
3
|
+
|
|
4
|
+
from biased.temporal.dtos import TemporalClientParams, TemporalParams
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TemporalUi:
|
|
8
|
+
@inject
|
|
9
|
+
def __init__(self, temporal_params: TemporalParams, client_params: TemporalClientParams):
|
|
10
|
+
self._temporal_params = temporal_params
|
|
11
|
+
self._client_params = client_params
|
|
12
|
+
|
|
13
|
+
def build_workflow_url(self, workflow_id: str) -> HttpUrl:
|
|
14
|
+
base = self._temporal_params.ui_base_url
|
|
15
|
+
assert base.host is not None # nosec B101:assert_used
|
|
16
|
+
return HttpUrl.build(
|
|
17
|
+
scheme=base.scheme,
|
|
18
|
+
host=base.host,
|
|
19
|
+
port=base.port,
|
|
20
|
+
path=f"namespaces/{self._client_params.namespace}/workflows/{workflow_id}",
|
|
21
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from injector import inject
|
|
2
|
+
from temporalio.client import Client
|
|
3
|
+
from temporalio.contrib.pydantic import pydantic_data_converter
|
|
4
|
+
|
|
5
|
+
from biased.temporal.dtos import TemporalClientParams
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TemporalClientHolder:
|
|
9
|
+
@inject
|
|
10
|
+
def __init__(self, params: TemporalClientParams) -> None:
|
|
11
|
+
self._params = params
|
|
12
|
+
self._client: Client | None = None
|
|
13
|
+
|
|
14
|
+
async def get_or_create_temporal_client(self) -> Client:
|
|
15
|
+
if self._client is None:
|
|
16
|
+
params = self._params
|
|
17
|
+
self._client = await Client.connect(
|
|
18
|
+
params.target_host,
|
|
19
|
+
namespace=params.namespace,
|
|
20
|
+
api_key=params.api_key,
|
|
21
|
+
identity=params.identity,
|
|
22
|
+
lazy=params.lazy,
|
|
23
|
+
data_converter=pydantic_data_converter,
|
|
24
|
+
)
|
|
25
|
+
return self._client
|
|
File without changes
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from temporalio.client import Client, WorkflowHandle
|
|
4
|
+
from temporalio.workflow import _Definition
|
|
5
|
+
|
|
6
|
+
from biased.dtos.base import BaseDto
|
|
7
|
+
from biased.temporal.workflows.utils import get_workflow_definition
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseWorkflow[ArgT: BaseDto, ResultT: BaseDto](ABC):
|
|
11
|
+
@classmethod
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def build_id(cls, arg: ArgT) -> str:
|
|
14
|
+
"""Generate a unique workflow ID based on the provided arg DTO."""
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def _get_workflow_definition(cls) -> _Definition:
|
|
19
|
+
"""Get the Temporal workflow definition for this workflow class."""
|
|
20
|
+
return get_workflow_definition(workflow=cls)
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def get_workflow_name(cls) -> str:
|
|
24
|
+
workflow_definition = cls._get_workflow_definition()
|
|
25
|
+
assert workflow_definition.name is not None # nosec B101:assert_used
|
|
26
|
+
return workflow_definition.name
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
async def start(
|
|
30
|
+
cls,
|
|
31
|
+
client: Client,
|
|
32
|
+
arg: ArgT,
|
|
33
|
+
*,
|
|
34
|
+
task_queue: str = "default",
|
|
35
|
+
id: str | None = None,
|
|
36
|
+
**kwargs,
|
|
37
|
+
) -> WorkflowHandle:
|
|
38
|
+
"""Start the workflow.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
client: Temporal client
|
|
42
|
+
arg: Workflow argument DTO
|
|
43
|
+
task_queue: Task queue name
|
|
44
|
+
id: Workflow ID. If None, will be generated using build_id
|
|
45
|
+
by extracting kwargs from arg.model_dump().
|
|
46
|
+
**kwargs: Additional kwargs passed to client.start_workflow.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
WorkflowHandle for the started workflow.
|
|
50
|
+
"""
|
|
51
|
+
workflow_definition = cls._get_workflow_definition()
|
|
52
|
+
if id is None:
|
|
53
|
+
id = cls.build_id(arg=arg)
|
|
54
|
+
handle = await client.start_workflow(
|
|
55
|
+
workflow_definition.run_fn,
|
|
56
|
+
arg=arg,
|
|
57
|
+
id=id,
|
|
58
|
+
task_queue=task_queue,
|
|
59
|
+
**kwargs,
|
|
60
|
+
)
|
|
61
|
+
return handle
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from temporalio import workflow
|
|
2
|
+
|
|
3
|
+
from biased.temporal.activities.dtos import EchoActivityArg, EchoActivityResult
|
|
4
|
+
from biased.temporal.activities.echo import echo_activity
|
|
5
|
+
from biased.temporal.workflows.base import BaseWorkflow
|
|
6
|
+
from biased.temporal.workflows.dtos import EchoWorkflowArg, EchoWorkflowResult
|
|
7
|
+
from biased.temporal.workflows.workflow import execute_activity
|
|
8
|
+
|
|
9
|
+
# Import activity, passing it through the sandbox without reloading the module
|
|
10
|
+
with workflow.unsafe.imports_passed_through():
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@workflow.defn(name="echo")
|
|
15
|
+
class EchoWorkflow(BaseWorkflow[EchoWorkflowArg, EchoWorkflowResult]):
|
|
16
|
+
@classmethod
|
|
17
|
+
def build_id(cls, arg: EchoWorkflowArg) -> str:
|
|
18
|
+
return f"{cls._get_workflow_definition().name}-{arg.message}"
|
|
19
|
+
|
|
20
|
+
@workflow.run
|
|
21
|
+
async def run(self, arg: EchoWorkflowArg) -> EchoWorkflowResult:
|
|
22
|
+
result: EchoActivityResult = await execute_activity(
|
|
23
|
+
echo_activity,
|
|
24
|
+
arg=EchoActivityArg(message=arg.message),
|
|
25
|
+
)
|
|
26
|
+
return EchoWorkflowResult(
|
|
27
|
+
message=result.message,
|
|
28
|
+
)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from temporalio.types import ClassType
|
|
2
|
+
from temporalio.workflow import _Definition
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_workflow_definition(workflow: ClassType) -> _Definition:
|
|
6
|
+
workflow_definition: _Definition = getattr(workflow, "__temporal_workflow_definition")
|
|
7
|
+
return workflow_definition
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
|
|
3
|
+
from asgiref.sync import async_to_sync
|
|
4
|
+
from temporalio import workflow
|
|
5
|
+
from temporalio.client import WorkflowHandle
|
|
6
|
+
|
|
7
|
+
from biased.dtos.base import BaseDto
|
|
8
|
+
from biased.temporal.activities.types import ActivityFuncWrapper
|
|
9
|
+
from biased.temporal.temporal_client_holder import TemporalClientHolder
|
|
10
|
+
from biased.temporal.workflows.base import BaseWorkflow
|
|
11
|
+
|
|
12
|
+
log = getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def execute_activity[ArgT: BaseDto, ResultT: BaseDto](
|
|
16
|
+
activity: ActivityFuncWrapper[ArgT, ResultT],
|
|
17
|
+
*,
|
|
18
|
+
arg: ArgT,
|
|
19
|
+
**kwargs,
|
|
20
|
+
) -> ResultT:
|
|
21
|
+
return await workflow.execute_activity(
|
|
22
|
+
activity=activity,
|
|
23
|
+
arg=arg,
|
|
24
|
+
start_to_close_timeout=activity.default_start_to_close_timeout,
|
|
25
|
+
retry_policy=activity.default_retry_policy,
|
|
26
|
+
**kwargs,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def execute_child_workflow[ArgT: BaseDto, ResultT: BaseDto](
|
|
31
|
+
child_workflow: type[BaseWorkflow[ArgT, ResultT]],
|
|
32
|
+
*,
|
|
33
|
+
arg: ArgT,
|
|
34
|
+
**kwargs,
|
|
35
|
+
) -> ResultT:
|
|
36
|
+
workflow_definition = child_workflow._get_workflow_definition()
|
|
37
|
+
return await workflow.execute_child_workflow(
|
|
38
|
+
workflow=workflow_definition.run_fn,
|
|
39
|
+
arg=arg,
|
|
40
|
+
id=child_workflow.build_id(arg=arg),
|
|
41
|
+
**kwargs,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def start_workflow[ArgT: BaseDto, ResultT: BaseDto](
|
|
46
|
+
temporal_client_holder: TemporalClientHolder,
|
|
47
|
+
workflow: type[BaseWorkflow[ArgT, ResultT]],
|
|
48
|
+
arg: ArgT,
|
|
49
|
+
*,
|
|
50
|
+
task_queue: str = "default",
|
|
51
|
+
**kwargs,
|
|
52
|
+
) -> WorkflowHandle:
|
|
53
|
+
@async_to_sync
|
|
54
|
+
async def _start() -> WorkflowHandle:
|
|
55
|
+
workflow_handle = await workflow.start(
|
|
56
|
+
client=await temporal_client_holder.get_or_create_temporal_client(),
|
|
57
|
+
arg=arg,
|
|
58
|
+
task_queue=task_queue,
|
|
59
|
+
**kwargs,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
log.info(
|
|
63
|
+
"started_temporal_workflow",
|
|
64
|
+
extra=dict(
|
|
65
|
+
data=dict(
|
|
66
|
+
workflow_name=workflow.get_workflow_name(),
|
|
67
|
+
workflow_handle_id=workflow_handle.id,
|
|
68
|
+
)
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
return workflow_handle
|
|
72
|
+
|
|
73
|
+
return _start()
|
|
File without changes
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "biased"
|
|
3
|
-
version = "0.1.2.dev13"
|
|
4
|
-
description = "Various reusable components, helpers, and utilities which I would like to use in any Python project"
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
authors = [
|
|
7
|
-
{ name = "Dmitry Tatarkin", email = "tatarkin@gmail.com" }
|
|
8
|
-
]
|
|
9
|
-
requires-python = ">=3.12"
|
|
10
|
-
dependencies = [
|
|
11
|
-
"injector>=0.22.0",
|
|
12
|
-
"pydantic>=2",
|
|
13
|
-
"python-dateutil>=2.9.0.post0",
|
|
14
|
-
"structlog>=25.4.0",
|
|
15
|
-
"ulid-py>=1.1.0",
|
|
16
|
-
]
|
|
17
|
-
|
|
18
|
-
[project.optional-dependencies]
|
|
19
|
-
django = [
|
|
20
|
-
"django>=5.2.6",
|
|
21
|
-
]
|
|
22
|
-
ninja = [
|
|
23
|
-
"django-ninja>=1.4.3",
|
|
24
|
-
]
|
|
25
|
-
django-redis = [
|
|
26
|
-
"django-redis>=6.0.0",
|
|
27
|
-
]
|
|
28
|
-
django-jsonform = [
|
|
29
|
-
"django-jsonform>=2.23.2",
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
[build-system]
|
|
33
|
-
requires = ["uv_build>=0.8.16,<0.9.0"]
|
|
34
|
-
build-backend = "uv_build"
|
|
35
|
-
|
|
36
|
-
[dependency-groups]
|
|
37
|
-
dev = [
|
|
38
|
-
"pytest>=9.0.2",
|
|
39
|
-
"rust-just>=1.43.0",
|
|
40
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/forms/pydantic_model_form_field.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/models/default_money_amount_field.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/templates/actions_change_form.html
RENAMED
|
File without changes
|
{biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/templates/admin/input_filter.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/biased-0.1.2.dev13/src/biased/pydantic/__init__.py → /biased-0.2.0.dev1/src/biased/py.typed
RENAMED
|
File without changes
|
{biased-0.1.2.dev13/src/biased/structlog → biased-0.2.0.dev1/src/biased/pydantic}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|