julee 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl

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 (155) hide show
  1. julee/api/app.py +9 -8
  2. julee/api/dependencies.py +15 -15
  3. julee/api/requests.py +10 -9
  4. julee/api/responses.py +2 -1
  5. julee/api/routers/__init__.py +5 -5
  6. julee/api/routers/assembly_specifications.py +5 -4
  7. julee/api/routers/documents.py +1 -1
  8. julee/api/routers/knowledge_service_configs.py +4 -3
  9. julee/api/routers/knowledge_service_queries.py +7 -6
  10. julee/api/routers/system.py +4 -3
  11. julee/api/routers/workflows.py +4 -5
  12. julee/api/services/system_initialization.py +6 -6
  13. julee/api/tests/routers/test_assembly_specifications.py +4 -3
  14. julee/api/tests/routers/test_documents.py +5 -4
  15. julee/api/tests/routers/test_knowledge_service_configs.py +7 -6
  16. julee/api/tests/routers/test_knowledge_service_queries.py +4 -3
  17. julee/api/tests/routers/test_system.py +5 -4
  18. julee/api/tests/routers/test_workflows.py +5 -4
  19. julee/api/tests/test_app.py +5 -4
  20. julee/api/tests/test_dependencies.py +3 -2
  21. julee/api/tests/test_requests.py +2 -1
  22. julee/contrib/__init__.py +15 -0
  23. julee/contrib/polling/__init__.py +47 -0
  24. julee/contrib/polling/domain/__init__.py +17 -0
  25. julee/contrib/polling/domain/models/__init__.py +13 -0
  26. julee/contrib/polling/domain/models/polling_config.py +39 -0
  27. julee/contrib/polling/domain/services/__init__.py +11 -0
  28. julee/contrib/polling/domain/services/poller.py +39 -0
  29. julee/contrib/polling/infrastructure/__init__.py +15 -0
  30. julee/contrib/polling/infrastructure/services/__init__.py +12 -0
  31. julee/contrib/polling/infrastructure/services/polling/__init__.py +12 -0
  32. julee/contrib/polling/infrastructure/services/polling/http/__init__.py +12 -0
  33. julee/contrib/polling/infrastructure/services/polling/http/http_poller_service.py +80 -0
  34. julee/contrib/polling/infrastructure/temporal/__init__.py +20 -0
  35. julee/contrib/polling/infrastructure/temporal/activities.py +42 -0
  36. julee/contrib/polling/infrastructure/temporal/activity_names.py +20 -0
  37. julee/contrib/polling/infrastructure/temporal/proxies.py +45 -0
  38. julee/contrib/polling/tests/__init__.py +6 -0
  39. julee/contrib/polling/tests/unit/__init__.py +6 -0
  40. julee/contrib/polling/tests/unit/infrastructure/__init__.py +7 -0
  41. julee/contrib/polling/tests/unit/infrastructure/services/__init__.py +6 -0
  42. julee/contrib/polling/tests/unit/infrastructure/services/polling/__init__.py +6 -0
  43. julee/contrib/polling/tests/unit/infrastructure/services/polling/http/__init__.py +7 -0
  44. julee/contrib/polling/tests/unit/infrastructure/services/polling/http/test_http_poller_service.py +163 -0
  45. julee/docs/__init__.py +5 -0
  46. julee/docs/sphinx_hcd/__init__.py +82 -0
  47. julee/docs/sphinx_hcd/accelerators.py +1078 -0
  48. julee/docs/sphinx_hcd/apps.py +499 -0
  49. julee/docs/sphinx_hcd/config.py +148 -0
  50. julee/docs/sphinx_hcd/epics.py +448 -0
  51. julee/docs/sphinx_hcd/integrations.py +306 -0
  52. julee/docs/sphinx_hcd/journeys.py +783 -0
  53. julee/docs/sphinx_hcd/personas.py +435 -0
  54. julee/docs/sphinx_hcd/stories.py +932 -0
  55. julee/docs/sphinx_hcd/utils.py +180 -0
  56. julee/domain/models/__init__.py +5 -6
  57. julee/domain/models/assembly/assembly.py +7 -7
  58. julee/domain/models/assembly/tests/factories.py +2 -1
  59. julee/domain/models/assembly/tests/test_assembly.py +16 -13
  60. julee/domain/models/assembly_specification/assembly_specification.py +11 -10
  61. julee/domain/models/assembly_specification/knowledge_service_query.py +7 -6
  62. julee/domain/models/assembly_specification/tests/factories.py +2 -1
  63. julee/domain/models/assembly_specification/tests/test_assembly_specification.py +9 -6
  64. julee/domain/models/assembly_specification/tests/test_knowledge_service_query.py +3 -1
  65. julee/domain/models/custom_fields/content_stream.py +3 -2
  66. julee/domain/models/custom_fields/tests/test_custom_fields.py +2 -1
  67. julee/domain/models/document/document.py +12 -10
  68. julee/domain/models/document/tests/factories.py +3 -2
  69. julee/domain/models/document/tests/test_document.py +6 -3
  70. julee/domain/models/knowledge_service_config/knowledge_service_config.py +4 -4
  71. julee/domain/models/policy/__init__.py +4 -4
  72. julee/domain/models/policy/document_policy_validation.py +17 -17
  73. julee/domain/models/policy/policy.py +10 -10
  74. julee/domain/models/policy/tests/factories.py +2 -1
  75. julee/domain/models/policy/tests/test_document_policy_validation.py +3 -1
  76. julee/domain/models/policy/tests/test_policy.py +2 -1
  77. julee/domain/repositories/__init__.py +3 -3
  78. julee/domain/repositories/assembly.py +3 -1
  79. julee/domain/repositories/assembly_specification.py +2 -0
  80. julee/domain/repositories/base.py +5 -4
  81. julee/domain/repositories/document.py +3 -1
  82. julee/domain/repositories/document_policy_validation.py +3 -1
  83. julee/domain/repositories/knowledge_service_config.py +2 -0
  84. julee/domain/repositories/knowledge_service_query.py +1 -0
  85. julee/domain/repositories/policy.py +3 -1
  86. julee/domain/use_cases/decorators.py +3 -2
  87. julee/domain/use_cases/extract_assemble_data.py +13 -12
  88. julee/domain/use_cases/initialize_system_data.py +13 -13
  89. julee/domain/use_cases/tests/test_extract_assemble_data.py +10 -10
  90. julee/domain/use_cases/tests/test_initialize_system_data.py +2 -2
  91. julee/domain/use_cases/tests/test_validate_document.py +11 -11
  92. julee/domain/use_cases/validate_document.py +14 -14
  93. julee/maintenance/__init__.py +1 -0
  94. julee/maintenance/release.py +188 -0
  95. julee/repositories/memory/assembly.py +6 -5
  96. julee/repositories/memory/assembly_specification.py +8 -9
  97. julee/repositories/memory/base.py +12 -11
  98. julee/repositories/memory/document.py +8 -7
  99. julee/repositories/memory/document_policy_validation.py +7 -6
  100. julee/repositories/memory/knowledge_service_config.py +8 -7
  101. julee/repositories/memory/knowledge_service_query.py +8 -7
  102. julee/repositories/memory/policy.py +6 -5
  103. julee/repositories/memory/tests/test_document.py +6 -4
  104. julee/repositories/memory/tests/test_document_policy_validation.py +2 -1
  105. julee/repositories/memory/tests/test_policy.py +2 -1
  106. julee/repositories/minio/assembly.py +4 -4
  107. julee/repositories/minio/assembly_specification.py +6 -8
  108. julee/repositories/minio/client.py +22 -25
  109. julee/repositories/minio/document.py +11 -11
  110. julee/repositories/minio/document_policy_validation.py +5 -5
  111. julee/repositories/minio/knowledge_service_config.py +6 -6
  112. julee/repositories/minio/knowledge_service_query.py +6 -9
  113. julee/repositories/minio/policy.py +4 -4
  114. julee/repositories/minio/tests/fake_client.py +11 -9
  115. julee/repositories/minio/tests/test_assembly.py +3 -1
  116. julee/repositories/minio/tests/test_assembly_specification.py +2 -1
  117. julee/repositories/minio/tests/test_client_protocol.py +5 -5
  118. julee/repositories/minio/tests/test_document.py +7 -6
  119. julee/repositories/minio/tests/test_document_policy_validation.py +3 -1
  120. julee/repositories/minio/tests/test_knowledge_service_config.py +4 -2
  121. julee/repositories/minio/tests/test_knowledge_service_query.py +3 -2
  122. julee/repositories/minio/tests/test_policy.py +3 -1
  123. julee/repositories/temporal/activities.py +5 -5
  124. julee/repositories/temporal/proxies.py +5 -5
  125. julee/services/knowledge_service/__init__.py +1 -2
  126. julee/services/knowledge_service/anthropic/knowledge_service.py +8 -7
  127. julee/services/knowledge_service/anthropic/tests/test_knowledge_service.py +11 -10
  128. julee/services/knowledge_service/factory.py +8 -8
  129. julee/services/knowledge_service/knowledge_service.py +12 -14
  130. julee/services/knowledge_service/memory/knowledge_service.py +13 -12
  131. julee/services/knowledge_service/memory/test_knowledge_service.py +10 -7
  132. julee/services/knowledge_service/test_factory.py +11 -10
  133. julee/services/temporal/activities.py +10 -10
  134. julee/services/temporal/proxies.py +2 -2
  135. julee/util/domain.py +6 -6
  136. julee/util/repos/minio/file_storage.py +8 -9
  137. julee/util/repos/temporal/client_proxies/file_storage.py +3 -4
  138. julee/util/repos/temporal/data_converter.py +6 -6
  139. julee/util/repos/temporal/minio_file_storage.py +1 -1
  140. julee/util/repos/temporal/proxies/file_storage.py +2 -3
  141. julee/util/repositories.py +4 -3
  142. julee/util/temporal/decorators.py +20 -18
  143. julee/util/tests/test_decorators.py +13 -15
  144. julee/util/validation/repository.py +3 -3
  145. julee/util/validation/type_guards.py +12 -11
  146. julee/worker.py +9 -8
  147. julee/workflows/__init__.py +2 -2
  148. julee/workflows/extract_assemble.py +2 -1
  149. julee/workflows/validate_document.py +3 -2
  150. {julee-0.1.2.dist-info → julee-0.1.3.dist-info}/METADATA +2 -1
  151. julee-0.1.3.dist-info/RECORD +197 -0
  152. julee-0.1.2.dist-info/RECORD +0 -161
  153. {julee-0.1.2.dist-info → julee-0.1.3.dist-info}/WHEEL +0 -0
  154. {julee-0.1.2.dist-info → julee-0.1.3.dist-info}/licenses/LICENSE +0 -0
  155. {julee-0.1.2.dist-info → julee-0.1.3.dist-info}/top_level.txt +0 -0
@@ -8,35 +8,35 @@ following the Clean Architecture principles.
8
8
 
9
9
  import io
10
10
  import json
11
- import pytest
12
-
13
- from unittest.mock import AsyncMock
14
11
  from datetime import datetime, timezone
12
+ from unittest.mock import AsyncMock
13
+
14
+ import pytest
15
15
 
16
- from julee.domain.use_cases import ExtractAssembleDataUseCase
17
16
  from julee.domain.models import (
18
17
  Assembly,
18
+ AssemblySpecification,
19
+ AssemblySpecificationStatus,
19
20
  AssemblyStatus,
21
+ ContentStream,
20
22
  Document,
21
23
  DocumentStatus,
22
- ContentStream,
23
- AssemblySpecification,
24
- AssemblySpecificationStatus,
25
- KnowledgeServiceQuery,
26
24
  KnowledgeServiceConfig,
25
+ KnowledgeServiceQuery,
27
26
  )
28
27
  from julee.domain.models.knowledge_service_config import ServiceApi
28
+ from julee.domain.use_cases import ExtractAssembleDataUseCase
29
29
  from julee.repositories.memory import (
30
- MemoryDocumentRepository,
31
30
  MemoryAssemblyRepository,
32
31
  MemoryAssemblySpecificationRepository,
32
+ MemoryDocumentRepository,
33
33
  MemoryKnowledgeServiceConfigRepository,
34
34
  MemoryKnowledgeServiceQueryRepository,
35
35
  )
36
+ from julee.services.knowledge_service import QueryResult
36
37
  from julee.services.knowledge_service.memory import (
37
38
  MemoryKnowledgeService,
38
39
  )
39
- from julee.services.knowledge_service import QueryResult
40
40
 
41
41
 
42
42
  class TestExtractAssembleDataUseCase:
@@ -86,7 +86,7 @@ def fixture_configs() -> list[dict]:
86
86
 
87
87
  assert fixture_path.exists(), f"Fixture file not found: {fixture_path}"
88
88
 
89
- with open(fixture_path, "r", encoding="utf-8") as f:
89
+ with open(fixture_path, encoding="utf-8") as f:
90
90
  fixture_data = yaml.safe_load(f)
91
91
 
92
92
  assert "knowledge_services" in fixture_data
@@ -320,7 +320,7 @@ class TestYamlFixtureIntegration:
320
320
  assert fixture_path.exists(), f"Fixture file not found: {fixture_path}"
321
321
 
322
322
  # Verify file can be parsed
323
- with open(fixture_path, "r", encoding="utf-8") as f:
323
+ with open(fixture_path, encoding="utf-8") as f:
324
324
  fixture_data = yaml.safe_load(f)
325
325
 
326
326
  # Verify structure
@@ -7,38 +7,38 @@ following the Clean Architecture principles.
7
7
  """
8
8
 
9
9
  import io
10
- import pytest
11
- from unittest.mock import AsyncMock
12
10
  from datetime import datetime, timezone
13
- from pydantic import ValidationError
11
+ from unittest.mock import AsyncMock
14
12
 
15
- from julee.domain.use_cases import ValidateDocumentUseCase
13
+ import pytest
14
+ from pydantic import ValidationError
16
15
 
17
16
  from julee.domain.models import (
17
+ ContentStream,
18
18
  Document,
19
19
  DocumentStatus,
20
- ContentStream,
21
20
  KnowledgeServiceConfig,
22
21
  KnowledgeServiceQuery,
23
22
  )
23
+ from julee.domain.models.knowledge_service_config import ServiceApi
24
24
  from julee.domain.models.policy import (
25
- Policy,
26
- PolicyStatus,
27
25
  DocumentPolicyValidation,
28
26
  DocumentPolicyValidationStatus,
27
+ Policy,
28
+ PolicyStatus,
29
29
  )
30
- from julee.domain.models.knowledge_service_config import ServiceApi
30
+ from julee.domain.use_cases import ValidateDocumentUseCase
31
31
  from julee.repositories.memory import (
32
+ MemoryDocumentPolicyValidationRepository,
32
33
  MemoryDocumentRepository,
33
- MemoryKnowledgeServiceQueryRepository,
34
34
  MemoryKnowledgeServiceConfigRepository,
35
+ MemoryKnowledgeServiceQueryRepository,
35
36
  MemoryPolicyRepository,
36
- MemoryDocumentPolicyValidationRepository,
37
37
  )
38
+ from julee.services.knowledge_service import QueryResult
38
39
  from julee.services.knowledge_service.memory import (
39
40
  MemoryKnowledgeService,
40
41
  )
41
- from julee.services.knowledge_service import QueryResult
42
42
 
43
43
 
44
44
  class TestValidateDocumentUseCase:
@@ -11,8 +11,8 @@ import hashlib
11
11
  import io
12
12
  import json
13
13
  import logging
14
+ from collections.abc import Callable
14
15
  from datetime import datetime
15
- from typing import Callable, Dict, List, Tuple
16
16
 
17
17
  import multihash
18
18
 
@@ -397,13 +397,13 @@ class ValidateDocumentUseCase:
397
397
  @try_use_case_step("all_queries_retrieval")
398
398
  async def _retrieve_all_queries(
399
399
  self, policy: Policy
400
- ) -> Dict[str, KnowledgeServiceQuery]:
400
+ ) -> dict[str, KnowledgeServiceQuery]:
401
401
  """Retrieve all knowledge service queries needed for validation and
402
402
  transformation."""
403
403
  all_queries = {}
404
404
 
405
405
  # Get validation queries
406
- for query_id, required_score in policy.validation_scores:
406
+ for query_id, _required_score in policy.validation_scores:
407
407
  query = await self.knowledge_service_query_repo.get(query_id)
408
408
  if not query:
409
409
  raise ValueError(f"Validation query not found: {query_id}")
@@ -423,8 +423,8 @@ class ValidateDocumentUseCase:
423
423
  async def _register_document_with_services(
424
424
  self,
425
425
  document: Document,
426
- queries: Dict[str, KnowledgeServiceQuery],
427
- ) -> Dict[str, str]:
426
+ queries: dict[str, KnowledgeServiceQuery],
427
+ ) -> dict[str, str]:
428
428
  """
429
429
  Register the document with all knowledge services needed for
430
430
  validation.
@@ -464,9 +464,9 @@ class ValidateDocumentUseCase:
464
464
  self,
465
465
  document: Document,
466
466
  policy: Policy,
467
- document_registrations: Dict[str, str],
468
- queries: Dict[str, KnowledgeServiceQuery],
469
- ) -> List[Tuple[str, int]]:
467
+ document_registrations: dict[str, str],
468
+ queries: dict[str, KnowledgeServiceQuery],
469
+ ) -> list[tuple[str, int]]:
470
470
  """
471
471
  Execute all validation queries and return the actual scores achieved.
472
472
 
@@ -528,7 +528,7 @@ class ValidateDocumentUseCase:
528
528
 
529
529
  return validation_scores
530
530
 
531
- def _extract_score_from_result(self, result_data: Dict) -> int:
531
+ def _extract_score_from_result(self, result_data: dict) -> int:
532
532
  """
533
533
  Extract a numeric score from the knowledge service query result.
534
534
 
@@ -551,8 +551,8 @@ class ValidateDocumentUseCase:
551
551
 
552
552
  def _determine_validation_result(
553
553
  self,
554
- actual_scores: List[Tuple[str, int]],
555
- required_scores: List[Tuple[str, int]],
554
+ actual_scores: list[tuple[str, int]],
555
+ required_scores: list[tuple[str, int]],
556
556
  ) -> bool:
557
557
  """
558
558
  Determine if validation passed based on actual vs required scores.
@@ -591,8 +591,8 @@ class ValidateDocumentUseCase:
591
591
  self,
592
592
  document: Document,
593
593
  policy: Policy,
594
- all_queries: Dict[str, KnowledgeServiceQuery],
595
- document_registrations: Dict[str, str],
594
+ all_queries: dict[str, KnowledgeServiceQuery],
595
+ document_registrations: dict[str, str],
596
596
  ) -> Document:
597
597
  """
598
598
  Apply transformation queries to a document and return the
@@ -714,7 +714,7 @@ class ValidateDocumentUseCase:
714
714
 
715
715
  return transformed_document
716
716
 
717
- def _extract_transformed_content(self, result_data: Dict) -> str:
717
+ def _extract_transformed_content(self, result_data: dict) -> str:
718
718
  """
719
719
  Extract transformed document content from knowledge service result.
720
720
 
@@ -0,0 +1 @@
1
+ """Maintenance utilities for julee and julee-based solutions."""
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Release preparation and tagging script.
4
+
5
+ Usage:
6
+ release.py prepare X.Y.Z # Create release branch and PR
7
+ release.py tag X.Y.Z # Tag after PR is merged
8
+ """
9
+
10
+ import re
11
+ import subprocess
12
+ import sys
13
+ from pathlib import Path
14
+
15
+
16
+ def run(cmd: str, check: bool = True, capture: bool = True) -> subprocess.CompletedProcess:
17
+ """Run a shell command."""
18
+ result = subprocess.run(cmd, shell=True, capture_output=capture, text=True)
19
+ if check and result.returncode != 0:
20
+ print(f"ERROR: {cmd}", file=sys.stderr)
21
+ if result.stderr:
22
+ print(result.stderr, file=sys.stderr)
23
+ sys.exit(1)
24
+ return result
25
+
26
+
27
+ def get_repo_root() -> Path:
28
+ """Get the repository root directory."""
29
+ result = run("git rev-parse --show-toplevel")
30
+ return Path(result.stdout.strip())
31
+
32
+
33
+ def get_package_init(repo_root: Path) -> Path | None:
34
+ """Find __init__.py with __version__ in src/ directory."""
35
+ src_dir = repo_root / "src"
36
+ if not src_dir.exists():
37
+ return None
38
+ packages = [p for p in src_dir.iterdir() if p.is_dir() and not p.name.startswith("_")]
39
+ if len(packages) != 1:
40
+ # Multiple packages (bounded contexts) - no single __init__.py to update
41
+ return None
42
+ init_file = packages[0] / "__init__.py"
43
+ if init_file.exists() and "__version__" in init_file.read_text():
44
+ return init_file
45
+ return None
46
+
47
+
48
+ def validate_version(version: str) -> None:
49
+ """Validate version string format."""
50
+ if not re.match(r"^\d+\.\d+\.\d+$", version):
51
+ print(f"ERROR: Invalid version format '{version}'. Expected X.Y.Z", file=sys.stderr)
52
+ sys.exit(1)
53
+
54
+
55
+ def validate_git_state(require_master: bool = True) -> None:
56
+ """Validate git working tree is clean and on correct branch."""
57
+ # Check for uncommitted changes
58
+ result = run("git status --porcelain")
59
+ if result.stdout.strip():
60
+ print("ERROR: Working tree has uncommitted changes", file=sys.stderr)
61
+ sys.exit(1)
62
+
63
+ if require_master:
64
+ # Check we're on master
65
+ result = run("git branch --show-current")
66
+ branch = result.stdout.strip()
67
+ if branch not in ("master", "main"):
68
+ print(f"ERROR: Must be on master or main branch, currently on '{branch}'", file=sys.stderr)
69
+ sys.exit(1)
70
+
71
+ # Check we're up to date with remote
72
+ run("git fetch origin")
73
+ result = run("git rev-list HEAD...origin/master --count 2>/dev/null || git rev-list HEAD...origin/main --count", check=False)
74
+ if result.stdout.strip() != "0":
75
+ print("ERROR: Branch is not up to date with remote", file=sys.stderr)
76
+ sys.exit(1)
77
+
78
+
79
+ def update_version_in_file(file_path: Path, version: str, pattern: str, replacement: str) -> None:
80
+ """Update version string in a file."""
81
+ content = file_path.read_text()
82
+ new_content = re.sub(pattern, replacement, content, flags=re.MULTILINE)
83
+ if content == new_content:
84
+ print(f"WARNING: No version replacement made in {file_path}", file=sys.stderr)
85
+ file_path.write_text(new_content)
86
+
87
+
88
+ def prepare(version: str) -> None:
89
+ """Prepare a release: create branch, update versions, push, create PR."""
90
+ validate_version(version)
91
+ validate_git_state(require_master=True)
92
+
93
+ repo_root = get_repo_root()
94
+ branch_name = f"release/v{version}"
95
+
96
+ # Create release branch
97
+ print(f"Creating branch {branch_name}...")
98
+ run(f"git checkout -b {branch_name}")
99
+
100
+ # Update pyproject.toml
101
+ pyproject = repo_root / "pyproject.toml"
102
+ print(f"Updating {pyproject}...")
103
+ update_version_in_file(
104
+ pyproject,
105
+ version,
106
+ r'^version\s*=\s*"[^"]*"',
107
+ f'version = "{version}"',
108
+ )
109
+
110
+ # Update __init__.py if it exists with __version__
111
+ init_file = get_package_init(repo_root)
112
+ if init_file:
113
+ print(f"Updating {init_file}...")
114
+ update_version_in_file(
115
+ init_file,
116
+ version,
117
+ r'^__version__\s*=\s*"[^"]*"',
118
+ f'__version__ = "{version}"',
119
+ )
120
+
121
+ # Commit
122
+ print("Committing version bump...")
123
+ run(f'git add -A && git commit -m "release: bump version to {version}"')
124
+
125
+ # Push
126
+ print(f"Pushing {branch_name}...")
127
+ run(f"git push -u origin {branch_name}")
128
+
129
+ # Create PR
130
+ print("Creating pull request...")
131
+ result = run(
132
+ f'gh pr create --title "Release v{version}" --body "Bump version to {version}"',
133
+ check=False,
134
+ )
135
+ if result.returncode != 0:
136
+ print(f"\nTo create PR manually:\n gh pr create --title 'Release v{version}'")
137
+
138
+ print(f"\nRelease branch ready. After PR is merged, run:\n ./maintenance/release.py tag {version}")
139
+
140
+
141
+ def tag(version: str) -> None:
142
+ """Tag a release after PR is merged."""
143
+ validate_version(version)
144
+
145
+ # Checkout master and pull
146
+ print("Checking out master...")
147
+ run("git checkout master || git checkout main")
148
+ run("git pull")
149
+
150
+ validate_git_state(require_master=True)
151
+
152
+ tag_name = f"v{version}"
153
+
154
+ # Check tag doesn't already exist
155
+ result = run(f"git tag -l {tag_name}")
156
+ if result.stdout.strip():
157
+ print(f"ERROR: Tag {tag_name} already exists", file=sys.stderr)
158
+ sys.exit(1)
159
+
160
+ # Create and push tag
161
+ print(f"Creating tag {tag_name}...")
162
+ run(f"git tag {tag_name}")
163
+ print(f"Pushing tag {tag_name}...")
164
+ run(f"git push origin {tag_name}")
165
+
166
+ print(f"\nRelease {tag_name} tagged and pushed.")
167
+
168
+
169
+ def main() -> None:
170
+ if len(sys.argv) < 3:
171
+ print(__doc__)
172
+ sys.exit(1)
173
+
174
+ command = sys.argv[1]
175
+ version = sys.argv[2]
176
+
177
+ if command == "prepare":
178
+ prepare(version)
179
+ elif command == "tag":
180
+ tag(version)
181
+ else:
182
+ print(f"Unknown command: {command}", file=sys.stderr)
183
+ print(__doc__)
184
+ sys.exit(1)
185
+
186
+
187
+ if __name__ == "__main__":
188
+ main()
@@ -12,10 +12,11 @@ All operations are still async to maintain interface compatibility.
12
12
  """
13
13
 
14
14
  import logging
15
- from typing import Optional, Dict, Any, List
15
+ from typing import Any
16
16
 
17
17
  from julee.domain.models.assembly import Assembly
18
18
  from julee.domain.repositories.assembly import AssemblyRepository
19
+
19
20
  from .base import MemoryRepositoryMixin
20
21
 
21
22
  logger = logging.getLogger(__name__)
@@ -34,11 +35,11 @@ class MemoryAssemblyRepository(AssemblyRepository, MemoryRepositoryMixin[Assembl
34
35
  """Initialize repository with empty in-memory storage."""
35
36
  self.logger = logger
36
37
  self.entity_name = "Assembly"
37
- self.storage_dict: Dict[str, Assembly] = {}
38
+ self.storage_dict: dict[str, Assembly] = {}
38
39
 
39
40
  logger.debug("Initializing MemoryAssemblyRepository")
40
41
 
41
- async def get(self, assembly_id: str) -> Optional[Assembly]:
42
+ async def get(self, assembly_id: str) -> Assembly | None:
42
43
  """Retrieve an assembly by ID.
43
44
 
44
45
  Args:
@@ -65,7 +66,7 @@ class MemoryAssemblyRepository(AssemblyRepository, MemoryRepositoryMixin[Assembl
65
66
  """
66
67
  return self.generate_entity_id("assembly")
67
68
 
68
- async def get_many(self, assembly_ids: List[str]) -> Dict[str, Optional[Assembly]]:
69
+ async def get_many(self, assembly_ids: list[str]) -> dict[str, Assembly | None]:
69
70
  """Retrieve multiple assemblies by ID.
70
71
 
71
72
  Args:
@@ -77,7 +78,7 @@ class MemoryAssemblyRepository(AssemblyRepository, MemoryRepositoryMixin[Assembl
77
78
  return self.get_many_entities(assembly_ids)
78
79
 
79
80
  def _add_entity_specific_log_data(
80
- self, entity: Assembly, log_data: Dict[str, Any]
81
+ self, entity: Assembly, log_data: dict[str, Any]
81
82
  ) -> None:
82
83
  """Add assembly-specific data to log entries."""
83
84
  super()._add_entity_specific_log_data(entity, log_data)
@@ -14,7 +14,7 @@ avoided. All operations are still async to maintain interface compatibility.
14
14
  """
15
15
 
16
16
  import logging
17
- from typing import Optional, Dict, Any, List
17
+ from typing import Any
18
18
 
19
19
  from julee.domain.models.assembly_specification import (
20
20
  AssemblySpecification,
@@ -22,6 +22,7 @@ from julee.domain.models.assembly_specification import (
22
22
  from julee.domain.repositories.assembly_specification import (
23
23
  AssemblySpecificationRepository,
24
24
  )
25
+
25
26
  from .base import MemoryRepositoryMixin
26
27
 
27
28
  logger = logging.getLogger(__name__)
@@ -48,13 +49,11 @@ class MemoryAssemblySpecificationRepository(
48
49
  """Initialize repository with empty in-memory storage."""
49
50
  self.logger = logger
50
51
  self.entity_name = "AssemblySpecification"
51
- self.storage_dict: Dict[str, AssemblySpecification] = {}
52
+ self.storage_dict: dict[str, AssemblySpecification] = {}
52
53
 
53
54
  logger.debug("Initializing MemoryAssemblySpecificationRepository")
54
55
 
55
- async def get(
56
- self, assembly_specification_id: str
57
- ) -> Optional[AssemblySpecification]:
56
+ async def get(self, assembly_specification_id: str) -> AssemblySpecification | None:
58
57
  """Retrieve an assembly specification by ID.
59
58
 
60
59
  Args:
@@ -82,8 +81,8 @@ class MemoryAssemblySpecificationRepository(
82
81
  return self.generate_entity_id("spec")
83
82
 
84
83
  async def get_many(
85
- self, assembly_specification_ids: List[str]
86
- ) -> Dict[str, Optional[AssemblySpecification]]:
84
+ self, assembly_specification_ids: list[str]
85
+ ) -> dict[str, AssemblySpecification | None]:
87
86
  """Retrieve multiple assembly specifications by ID.
88
87
 
89
88
  Args:
@@ -96,7 +95,7 @@ class MemoryAssemblySpecificationRepository(
96
95
  """
97
96
  return self.get_many_entities(assembly_specification_ids)
98
97
 
99
- async def list_all(self) -> List[AssemblySpecification]:
98
+ async def list_all(self) -> list[AssemblySpecification]:
100
99
  """List all assembly specifications.
101
100
 
102
101
  Returns:
@@ -118,7 +117,7 @@ class MemoryAssemblySpecificationRepository(
118
117
  return specifications
119
118
 
120
119
  def _add_entity_specific_log_data(
121
- self, entity: AssemblySpecification, log_data: Dict[str, Any]
120
+ self, entity: AssemblySpecification, log_data: dict[str, Any]
122
121
  ) -> None:
123
122
  """Add assembly specification-specific data to log entries."""
124
123
  super()._add_entity_specific_log_data(entity, log_data)
@@ -20,7 +20,8 @@ Classes using this mixin must provide:
20
20
 
21
21
  import uuid
22
22
  from datetime import datetime, timezone
23
- from typing import Optional, Dict, Any, TypeVar, Generic, List
23
+ from typing import Any, Generic, TypeVar
24
+
24
25
  from pydantic import BaseModel
25
26
 
26
27
  T = TypeVar("T", bound=BaseModel)
@@ -45,11 +46,11 @@ class MemoryRepositoryMixin(Generic[T]):
45
46
  """
46
47
 
47
48
  # Type annotations for attributes that implementing classes must provide
48
- storage_dict: Dict[str, T]
49
+ storage_dict: dict[str, T]
49
50
  entity_name: str
50
51
  logger: Any # logging.Logger, but avoiding import
51
52
 
52
- def get_entity(self, entity_id: str) -> Optional[T]:
53
+ def get_entity(self, entity_id: str) -> T | None:
53
54
  """Get an entity from memory storage with standardized logging.
54
55
 
55
56
  Args:
@@ -84,7 +85,7 @@ class MemoryRepositoryMixin(Generic[T]):
84
85
 
85
86
  return entity
86
87
 
87
- def get_many_entities(self, entity_ids: List[str]) -> Dict[str, Optional[T]]:
88
+ def get_many_entities(self, entity_ids: list[str]) -> dict[str, T | None]:
88
89
  """Get multiple entities from memory storage with standardized
89
90
  logging.
90
91
 
@@ -103,7 +104,7 @@ class MemoryRepositoryMixin(Generic[T]):
103
104
  },
104
105
  )
105
106
 
106
- result: Dict[str, Optional[T]] = {}
107
+ result: dict[str, T | None] = {}
107
108
  found_count = 0
108
109
 
109
110
  for entity_id in entity_ids:
@@ -160,7 +161,7 @@ class MemoryRepositoryMixin(Generic[T]):
160
161
  extra=success_extra,
161
162
  )
162
163
 
163
- def generate_entity_id(self, prefix: Optional[str] = None) -> str:
164
+ def generate_entity_id(self, prefix: str | None = None) -> str:
164
165
  """Generate a unique entity ID with consistent format.
165
166
 
166
167
  Args:
@@ -196,14 +197,14 @@ class MemoryRepositoryMixin(Generic[T]):
196
197
  hasattr(entity, "created_at")
197
198
  and getattr(entity, "created_at", None) is None
198
199
  ):
199
- setattr(entity, "created_at", now)
200
+ entity.created_at = now
200
201
 
201
202
  # Always update updated_at
202
203
  if hasattr(entity, "updated_at"):
203
- setattr(entity, "updated_at", now)
204
+ entity.updated_at = now
204
205
 
205
206
  def _add_entity_specific_log_data(
206
- self, entity: T, log_data: Dict[str, Any]
207
+ self, entity: T, log_data: dict[str, Any]
207
208
  ) -> None:
208
209
  """Add entity-specific data to log entries for richer logging.
209
210
 
@@ -216,12 +217,12 @@ class MemoryRepositoryMixin(Generic[T]):
216
217
  """
217
218
  # Default implementation adds basic model info
218
219
  if hasattr(entity, "status"):
219
- status = getattr(entity, "status")
220
+ status = entity.status
220
221
  log_data["status"] = (
221
222
  status.value if hasattr(status, "value") else str(status)
222
223
  )
223
224
 
224
225
  if hasattr(entity, "updated_at"):
225
- updated_at = getattr(entity, "updated_at")
226
+ updated_at = entity.updated_at
226
227
  if updated_at:
227
228
  log_data["updated_at"] = updated_at.isoformat()
@@ -14,13 +14,14 @@ All operations are still async to maintain interface compatibility.
14
14
  import hashlib
15
15
  import io
16
16
  import logging
17
- from typing import Optional, Dict, Any, List
17
+ from typing import Any
18
18
 
19
- from julee.domain.models.document import Document
20
19
  from julee.domain.models.custom_fields.content_stream import (
21
20
  ContentStream,
22
21
  )
22
+ from julee.domain.models.document import Document
23
23
  from julee.domain.repositories.document import DocumentRepository
24
+
24
25
  from .base import MemoryRepositoryMixin
25
26
 
26
27
  logger = logging.getLogger(__name__)
@@ -41,11 +42,11 @@ class MemoryDocumentRepository(DocumentRepository, MemoryRepositoryMixin[Documen
41
42
  """Initialize repository with empty in-memory storage."""
42
43
  self.logger = logger
43
44
  self.entity_name = "Document"
44
- self.storage_dict: Dict[str, Document] = {}
45
+ self.storage_dict: dict[str, Document] = {}
45
46
 
46
47
  logger.debug("Initializing MemoryDocumentRepository")
47
48
 
48
- async def get(self, document_id: str) -> Optional[Document]:
49
+ async def get(self, document_id: str) -> Document | None:
49
50
  """Retrieve a document with metadata and content.
50
51
 
51
52
  Args:
@@ -109,7 +110,7 @@ class MemoryDocumentRepository(DocumentRepository, MemoryRepositoryMixin[Documen
109
110
  """
110
111
  return self.generate_entity_id("doc")
111
112
 
112
- async def get_many(self, document_ids: List[str]) -> Dict[str, Optional[Document]]:
113
+ async def get_many(self, document_ids: list[str]) -> dict[str, Document | None]:
113
114
  """Retrieve multiple documents by ID.
114
115
 
115
116
  Args:
@@ -120,7 +121,7 @@ class MemoryDocumentRepository(DocumentRepository, MemoryRepositoryMixin[Documen
120
121
  """
121
122
  return self.get_many_entities(document_ids)
122
123
 
123
- async def list_all(self) -> List[Document]:
124
+ async def list_all(self) -> list[Document]:
124
125
  """List all documents.
125
126
 
126
127
  Returns:
@@ -142,7 +143,7 @@ class MemoryDocumentRepository(DocumentRepository, MemoryRepositoryMixin[Documen
142
143
  return documents
143
144
 
144
145
  def _add_entity_specific_log_data(
145
- self, entity: Document, log_data: Dict[str, Any]
146
+ self, entity: Document, log_data: dict[str, Any]
146
147
  ) -> None:
147
148
  """Add document-specific data to log entries."""
148
149
  super()._add_entity_specific_log_data(entity, log_data)