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.
Files changed (93) hide show
  1. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/PKG-INFO +12 -9
  2. biased-0.2.0.dev1/pyproject.toml +76 -0
  3. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/admin.py +6 -6
  4. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/aws_waf/aws_waf_regex_pattern.py +2 -2
  5. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/aws_waf/aws_waf_whitelist_middleware.py +2 -2
  6. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/aws_waf/url_patterns.py +8 -7
  7. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/filters/entity_filter.py +3 -0
  8. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/filters/input_filter.py +17 -12
  9. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/models/created_at_modified_at_mixin.py +2 -2
  10. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/models/deleted_at_mixin.py +2 -2
  11. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/utils/choices.py +1 -1
  12. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/utils/get_client_ip.py +1 -1
  13. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django_jsonform/jsonform_action.py +1 -6
  14. biased-0.2.0.dev1/src/biased/scripts/dotenv_run.py +45 -0
  15. biased-0.2.0.dev1/src/biased/structlog/__init__.py +0 -0
  16. biased-0.2.0.dev1/src/biased/temporal/__init__.py +0 -0
  17. biased-0.2.0.dev1/src/biased/temporal/activities/__init__.py +0 -0
  18. biased-0.2.0.dev1/src/biased/temporal/activities/consts.py +11 -0
  19. biased-0.2.0.dev1/src/biased/temporal/activities/dtos.py +11 -0
  20. biased-0.2.0.dev1/src/biased/temporal/activities/echo.py +12 -0
  21. biased-0.2.0.dev1/src/biased/temporal/activities/types.py +16 -0
  22. biased-0.2.0.dev1/src/biased/temporal/activities/utils.py +50 -0
  23. biased-0.2.0.dev1/src/biased/temporal/django/__init__.py +0 -0
  24. biased-0.2.0.dev1/src/biased/temporal/django/admin/__init__.py +0 -0
  25. biased-0.2.0.dev1/src/biased/temporal/django/admin/trigger_temporal_workflow_mixin.py +65 -0
  26. biased-0.2.0.dev1/src/biased/temporal/django/temporal_ui.py +11 -0
  27. biased-0.2.0.dev1/src/biased/temporal/dtos.py +25 -0
  28. biased-0.2.0.dev1/src/biased/temporal/services/__init__.py +0 -0
  29. biased-0.2.0.dev1/src/biased/temporal/services/temporal_ui.py +21 -0
  30. biased-0.2.0.dev1/src/biased/temporal/temporal_client_holder.py +25 -0
  31. biased-0.2.0.dev1/src/biased/temporal/workflows/__init__.py +0 -0
  32. biased-0.2.0.dev1/src/biased/temporal/workflows/base.py +61 -0
  33. biased-0.2.0.dev1/src/biased/temporal/workflows/dtos.py +11 -0
  34. biased-0.2.0.dev1/src/biased/temporal/workflows/echo.py +28 -0
  35. biased-0.2.0.dev1/src/biased/temporal/workflows/utils.py +7 -0
  36. biased-0.2.0.dev1/src/biased/temporal/workflows/workflow.py +73 -0
  37. biased-0.2.0.dev1/src/biased/utils/__init__.py +0 -0
  38. biased-0.1.2.dev13/pyproject.toml +0 -40
  39. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/README.md +0 -0
  40. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/__init__.py +0 -0
  41. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/consts.py +0 -0
  42. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/__init__.py +0 -0
  43. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/apps.py +0 -0
  44. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/aws_waf/__init__.py +0 -0
  45. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/dtos.py +0 -0
  46. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/filters/__init__.py +0 -0
  47. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/forms/__init__.py +0 -0
  48. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/forms/json_editor_widget.py +0 -0
  49. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/forms/pydantic_model_form_field.py +0 -0
  50. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/forms/ulid_form_field.py +0 -0
  51. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/models/__init__.py +0 -0
  52. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/models/bank_account.py +0 -0
  53. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/models/char_field.py +0 -0
  54. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/models/default_money_amount_field.py +0 -0
  55. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/models/multi_select_field.py +0 -0
  56. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/models/pydantic_model_field.py +0 -0
  57. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/templates/actions_change_form.html +0 -0
  58. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/templates/admin/input_filter.html +0 -0
  59. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/utils/__init__.py +0 -0
  60. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/utils/get_user_agent.py +0 -0
  61. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/utils/reverse.py +0 -0
  62. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/validators/__init__.py +0 -0
  63. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django/validators/pydantic.py +0 -0
  64. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django_jsonform/__init__.py +0 -0
  65. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django_jsonform/apps.py +0 -0
  66. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django_jsonform/templates/admin_form_action/django_jsonform_form.html +0 -0
  67. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django_redis/__init__.py +0 -0
  68. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/django_redis/pydantic_serializer.py +0 -0
  69. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/dtos/__init__.py +0 -0
  70. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/dtos/base.py +0 -0
  71. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/dtos/env_file_paths.py +0 -0
  72. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/dtos/logging.py +0 -0
  73. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/logging/__init__.py +0 -0
  74. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/logging/dict_config.py +0 -0
  75. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/logging/settings.py +0 -0
  76. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/logging/utils.py +0 -0
  77. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/ninja/__init__.py +0 -0
  78. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/ninja/json_renderrer.py +0 -0
  79. /biased-0.1.2.dev13/src/biased/pydantic/__init__.py → /biased-0.2.0.dev1/src/biased/py.typed +0 -0
  80. {biased-0.1.2.dev13/src/biased/structlog → biased-0.2.0.dev1/src/biased/pydantic}/__init__.py +0 -0
  81. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/pydantic/base64_pydantic_model.py +0 -0
  82. {biased-0.1.2.dev13/src/biased/utils → biased-0.2.0.dev1/src/biased/scripts}/__init__.py +0 -0
  83. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/structlog/processors.py +0 -0
  84. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/structlog/structlog_json_encoder.py +0 -0
  85. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/types.py +0 -0
  86. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/utils/aba_routing_number.py +0 -0
  87. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/utils/attrgetter_default.py +0 -0
  88. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/utils/calculate_age.py +0 -0
  89. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/utils/default_json_encoder.py +0 -0
  90. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/utils/filter_not_none_values.py +0 -0
  91. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/utils/time.py +0 -0
  92. {biased-0.1.2.dev13 → biased-0.2.0.dev1}/src/biased/utils/ulid.py +0 -0
  93. {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.1.2.dev13
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>=0.22.0
7
+ Requires-Dist: injector
8
8
  Requires-Dist: pydantic>=2
9
- Requires-Dist: python-dateutil>=2.9.0.post0
10
- Requires-Dist: structlog>=25.4.0
11
- Requires-Dist: ulid-py>=1.1.0
12
- Requires-Dist: django>=5.2.6 ; extra == 'django'
13
- Requires-Dist: django-jsonform>=2.23.2 ; extra == 'django-jsonform'
14
- Requires-Dist: django-redis>=6.0.0 ; extra == 'django-redis'
15
- Requires-Dist: django-ninja>=1.4.3 ; extra == 'ninja'
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
- url_params = set(url_params)
39
+ url_params_set = set(url_params)
40
40
  with context(url=url):
41
- remaining_params = url_params - context.keys()
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:
@@ -1,5 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
- from collections.abc import Callable, Sequence
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, Callable[[HttpRequest], HttpResponse]):
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[tuple[str, dict[str, Any]]]) -> None:
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[tuple[RoutePattern, ...]]:
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.db.models import Q
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
- if self.value() is not None:
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 self.value().split(","):
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
- if self.value() is not None:
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 self.value().split(","):
57
+ for i in filter_value.split(","):
53
58
  value = i.strip()
54
59
  if not value:
55
60
  continue
@@ -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
@@ -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,11 @@
1
+ from pydantic import StrictStr
2
+
3
+ from biased.dtos.base import BaseDto
4
+
5
+
6
+ class EchoActivityArg(BaseDto):
7
+ message: StrictStr
8
+
9
+
10
+ class EchoActivityResult(BaseDto):
11
+ message: StrictStr
@@ -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
@@ -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="__")
@@ -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
@@ -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,11 @@
1
+ from pydantic import StrictStr
2
+
3
+ from biased.dtos.base import BaseDto
4
+
5
+
6
+ class EchoWorkflowArg(BaseDto):
7
+ message: StrictStr
8
+
9
+
10
+ class EchoWorkflowResult(BaseDto):
11
+ message: StrictStr
@@ -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