julee 0.1.4__py3-none-any.whl → 0.1.6__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 (165) hide show
  1. julee/__init__.py +1 -1
  2. julee/api/tests/routers/test_assembly_specifications.py +2 -0
  3. julee/api/tests/routers/test_documents.py +2 -0
  4. julee/api/tests/routers/test_knowledge_service_configs.py +2 -0
  5. julee/api/tests/routers/test_knowledge_service_queries.py +2 -0
  6. julee/api/tests/routers/test_system.py +2 -0
  7. julee/api/tests/routers/test_workflows.py +2 -0
  8. julee/api/tests/test_app.py +2 -0
  9. julee/api/tests/test_dependencies.py +2 -0
  10. julee/api/tests/test_requests.py +2 -0
  11. julee/contrib/polling/__init__.py +22 -19
  12. julee/contrib/polling/apps/__init__.py +17 -0
  13. julee/contrib/polling/apps/worker/__init__.py +17 -0
  14. julee/contrib/polling/apps/worker/pipelines.py +288 -0
  15. julee/contrib/polling/domain/__init__.py +7 -9
  16. julee/contrib/polling/domain/models/__init__.py +6 -7
  17. julee/contrib/polling/domain/models/polling_config.py +18 -1
  18. julee/contrib/polling/domain/services/__init__.py +6 -5
  19. julee/contrib/polling/domain/services/poller.py +1 -1
  20. julee/contrib/polling/infrastructure/__init__.py +9 -8
  21. julee/contrib/polling/infrastructure/services/__init__.py +6 -5
  22. julee/contrib/polling/infrastructure/services/polling/__init__.py +6 -5
  23. julee/contrib/polling/infrastructure/services/polling/http/__init__.py +6 -5
  24. julee/contrib/polling/infrastructure/services/polling/http/http_poller_service.py +5 -2
  25. julee/contrib/polling/infrastructure/temporal/__init__.py +12 -12
  26. julee/contrib/polling/infrastructure/temporal/activities.py +1 -1
  27. julee/contrib/polling/infrastructure/temporal/manager.py +291 -0
  28. julee/contrib/polling/infrastructure/temporal/proxies.py +1 -1
  29. julee/contrib/polling/tests/unit/apps/worker/test_pipelines.py +580 -0
  30. julee/contrib/polling/tests/unit/infrastructure/services/polling/http/test_http_poller_service.py +40 -2
  31. julee/contrib/polling/tests/unit/infrastructure/temporal/__init__.py +7 -0
  32. julee/contrib/polling/tests/unit/infrastructure/temporal/test_manager.py +475 -0
  33. julee/docs/sphinx_hcd/__init__.py +146 -13
  34. julee/docs/sphinx_hcd/domain/__init__.py +5 -0
  35. julee/docs/sphinx_hcd/domain/models/__init__.py +32 -0
  36. julee/docs/sphinx_hcd/domain/models/accelerator.py +152 -0
  37. julee/docs/sphinx_hcd/domain/models/app.py +151 -0
  38. julee/docs/sphinx_hcd/domain/models/code_info.py +121 -0
  39. julee/docs/sphinx_hcd/domain/models/epic.py +79 -0
  40. julee/docs/sphinx_hcd/domain/models/integration.py +230 -0
  41. julee/docs/sphinx_hcd/domain/models/journey.py +222 -0
  42. julee/docs/sphinx_hcd/domain/models/persona.py +106 -0
  43. julee/docs/sphinx_hcd/domain/models/story.py +128 -0
  44. julee/docs/sphinx_hcd/domain/repositories/__init__.py +25 -0
  45. julee/docs/sphinx_hcd/domain/repositories/accelerator.py +98 -0
  46. julee/docs/sphinx_hcd/domain/repositories/app.py +57 -0
  47. julee/docs/sphinx_hcd/domain/repositories/base.py +89 -0
  48. julee/docs/sphinx_hcd/domain/repositories/code_info.py +69 -0
  49. julee/docs/sphinx_hcd/domain/repositories/epic.py +62 -0
  50. julee/docs/sphinx_hcd/domain/repositories/integration.py +79 -0
  51. julee/docs/sphinx_hcd/domain/repositories/journey.py +106 -0
  52. julee/docs/sphinx_hcd/domain/repositories/story.py +68 -0
  53. julee/docs/sphinx_hcd/domain/use_cases/__init__.py +64 -0
  54. julee/docs/sphinx_hcd/domain/use_cases/derive_personas.py +166 -0
  55. julee/docs/sphinx_hcd/domain/use_cases/resolve_accelerator_references.py +236 -0
  56. julee/docs/sphinx_hcd/domain/use_cases/resolve_app_references.py +144 -0
  57. julee/docs/sphinx_hcd/domain/use_cases/resolve_story_references.py +121 -0
  58. julee/docs/sphinx_hcd/parsers/__init__.py +48 -0
  59. julee/docs/sphinx_hcd/parsers/ast.py +150 -0
  60. julee/docs/sphinx_hcd/parsers/gherkin.py +155 -0
  61. julee/docs/sphinx_hcd/parsers/yaml.py +184 -0
  62. julee/docs/sphinx_hcd/repositories/__init__.py +4 -0
  63. julee/docs/sphinx_hcd/repositories/memory/__init__.py +25 -0
  64. julee/docs/sphinx_hcd/repositories/memory/accelerator.py +86 -0
  65. julee/docs/sphinx_hcd/repositories/memory/app.py +45 -0
  66. julee/docs/sphinx_hcd/repositories/memory/base.py +106 -0
  67. julee/docs/sphinx_hcd/repositories/memory/code_info.py +59 -0
  68. julee/docs/sphinx_hcd/repositories/memory/epic.py +54 -0
  69. julee/docs/sphinx_hcd/repositories/memory/integration.py +70 -0
  70. julee/docs/sphinx_hcd/repositories/memory/journey.py +96 -0
  71. julee/docs/sphinx_hcd/repositories/memory/story.py +63 -0
  72. julee/docs/sphinx_hcd/sphinx/__init__.py +28 -0
  73. julee/docs/sphinx_hcd/sphinx/adapters.py +116 -0
  74. julee/docs/sphinx_hcd/sphinx/context.py +163 -0
  75. julee/docs/sphinx_hcd/sphinx/directives/__init__.py +160 -0
  76. julee/docs/sphinx_hcd/sphinx/directives/accelerator.py +576 -0
  77. julee/docs/sphinx_hcd/sphinx/directives/app.py +349 -0
  78. julee/docs/sphinx_hcd/sphinx/directives/base.py +211 -0
  79. julee/docs/sphinx_hcd/sphinx/directives/epic.py +434 -0
  80. julee/docs/sphinx_hcd/sphinx/directives/integration.py +220 -0
  81. julee/docs/sphinx_hcd/sphinx/directives/journey.py +642 -0
  82. julee/docs/sphinx_hcd/sphinx/directives/persona.py +345 -0
  83. julee/docs/sphinx_hcd/sphinx/directives/story.py +575 -0
  84. julee/docs/sphinx_hcd/sphinx/event_handlers/__init__.py +16 -0
  85. julee/docs/sphinx_hcd/sphinx/event_handlers/builder_inited.py +31 -0
  86. julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_read.py +27 -0
  87. julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_resolved.py +43 -0
  88. julee/docs/sphinx_hcd/sphinx/event_handlers/env_purge_doc.py +42 -0
  89. julee/docs/sphinx_hcd/sphinx/initialization.py +139 -0
  90. julee/docs/sphinx_hcd/tests/__init__.py +9 -0
  91. julee/docs/sphinx_hcd/tests/conftest.py +6 -0
  92. julee/docs/sphinx_hcd/tests/domain/__init__.py +1 -0
  93. julee/docs/sphinx_hcd/tests/domain/models/__init__.py +1 -0
  94. julee/docs/sphinx_hcd/tests/domain/models/test_accelerator.py +266 -0
  95. julee/docs/sphinx_hcd/tests/domain/models/test_app.py +258 -0
  96. julee/docs/sphinx_hcd/tests/domain/models/test_code_info.py +231 -0
  97. julee/docs/sphinx_hcd/tests/domain/models/test_epic.py +163 -0
  98. julee/docs/sphinx_hcd/tests/domain/models/test_integration.py +327 -0
  99. julee/docs/sphinx_hcd/tests/domain/models/test_journey.py +249 -0
  100. julee/docs/sphinx_hcd/tests/domain/models/test_persona.py +172 -0
  101. julee/docs/sphinx_hcd/tests/domain/models/test_story.py +216 -0
  102. julee/docs/sphinx_hcd/tests/domain/use_cases/__init__.py +1 -0
  103. julee/docs/sphinx_hcd/tests/domain/use_cases/test_derive_personas.py +314 -0
  104. julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_accelerator_references.py +476 -0
  105. julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_app_references.py +265 -0
  106. julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_story_references.py +229 -0
  107. julee/docs/sphinx_hcd/tests/integration/__init__.py +1 -0
  108. julee/docs/sphinx_hcd/tests/parsers/__init__.py +1 -0
  109. julee/docs/sphinx_hcd/tests/parsers/test_ast.py +298 -0
  110. julee/docs/sphinx_hcd/tests/parsers/test_gherkin.py +282 -0
  111. julee/docs/sphinx_hcd/tests/parsers/test_yaml.py +496 -0
  112. julee/docs/sphinx_hcd/tests/repositories/__init__.py +1 -0
  113. julee/docs/sphinx_hcd/tests/repositories/test_accelerator.py +298 -0
  114. julee/docs/sphinx_hcd/tests/repositories/test_app.py +218 -0
  115. julee/docs/sphinx_hcd/tests/repositories/test_base.py +151 -0
  116. julee/docs/sphinx_hcd/tests/repositories/test_code_info.py +253 -0
  117. julee/docs/sphinx_hcd/tests/repositories/test_epic.py +237 -0
  118. julee/docs/sphinx_hcd/tests/repositories/test_integration.py +268 -0
  119. julee/docs/sphinx_hcd/tests/repositories/test_journey.py +294 -0
  120. julee/docs/sphinx_hcd/tests/repositories/test_story.py +236 -0
  121. julee/docs/sphinx_hcd/tests/sphinx/__init__.py +1 -0
  122. julee/docs/sphinx_hcd/tests/sphinx/directives/__init__.py +1 -0
  123. julee/docs/sphinx_hcd/tests/sphinx/directives/test_base.py +160 -0
  124. julee/docs/sphinx_hcd/tests/sphinx/test_adapters.py +176 -0
  125. julee/docs/sphinx_hcd/tests/sphinx/test_context.py +257 -0
  126. julee/domain/models/assembly/tests/test_assembly.py +2 -0
  127. julee/domain/models/assembly_specification/tests/test_assembly_specification.py +2 -0
  128. julee/domain/models/assembly_specification/tests/test_knowledge_service_query.py +2 -0
  129. julee/domain/models/custom_fields/tests/test_custom_fields.py +2 -0
  130. julee/domain/models/document/tests/test_document.py +2 -0
  131. julee/domain/models/policy/tests/test_document_policy_validation.py +2 -0
  132. julee/domain/models/policy/tests/test_policy.py +2 -0
  133. julee/domain/use_cases/tests/test_extract_assemble_data.py +2 -0
  134. julee/domain/use_cases/tests/test_initialize_system_data.py +2 -0
  135. julee/domain/use_cases/tests/test_validate_document.py +2 -0
  136. julee/maintenance/release.py +10 -5
  137. julee/repositories/memory/tests/test_document.py +2 -0
  138. julee/repositories/memory/tests/test_document_policy_validation.py +2 -0
  139. julee/repositories/memory/tests/test_policy.py +2 -0
  140. julee/repositories/minio/tests/test_assembly.py +2 -0
  141. julee/repositories/minio/tests/test_assembly_specification.py +2 -0
  142. julee/repositories/minio/tests/test_client_protocol.py +3 -0
  143. julee/repositories/minio/tests/test_document.py +2 -0
  144. julee/repositories/minio/tests/test_document_policy_validation.py +2 -0
  145. julee/repositories/minio/tests/test_knowledge_service_config.py +2 -0
  146. julee/repositories/minio/tests/test_knowledge_service_query.py +2 -0
  147. julee/repositories/minio/tests/test_policy.py +2 -0
  148. julee/services/knowledge_service/anthropic/tests/test_knowledge_service.py +2 -0
  149. julee/services/knowledge_service/memory/test_knowledge_service.py +2 -0
  150. julee/services/knowledge_service/test_factory.py +2 -0
  151. julee/util/tests/test_decorators.py +2 -0
  152. julee-0.1.6.dist-info/METADATA +104 -0
  153. julee-0.1.6.dist-info/RECORD +288 -0
  154. julee/docs/sphinx_hcd/accelerators.py +0 -1175
  155. julee/docs/sphinx_hcd/apps.py +0 -518
  156. julee/docs/sphinx_hcd/epics.py +0 -453
  157. julee/docs/sphinx_hcd/integrations.py +0 -310
  158. julee/docs/sphinx_hcd/journeys.py +0 -797
  159. julee/docs/sphinx_hcd/personas.py +0 -457
  160. julee/docs/sphinx_hcd/stories.py +0 -960
  161. julee-0.1.4.dist-info/METADATA +0 -197
  162. julee-0.1.4.dist-info/RECORD +0 -196
  163. {julee-0.1.4.dist-info → julee-0.1.6.dist-info}/WHEEL +0 -0
  164. {julee-0.1.4.dist-info → julee-0.1.6.dist-info}/licenses/LICENSE +0 -0
  165. {julee-0.1.4.dist-info → julee-0.1.6.dist-info}/top_level.txt +0 -0
julee/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Julee - Clean architecture for accountable and transparent digital supply chains."""
2
2
 
3
- __version__ = "0.1.1"
3
+ __version__ = "0.1.5"
@@ -25,6 +25,8 @@ from julee.repositories.memory import (
25
25
  MemoryAssemblySpecificationRepository,
26
26
  )
27
27
 
28
+ pytestmark = pytest.mark.unit
29
+
28
30
 
29
31
  @pytest.fixture
30
32
  def memory_repo() -> MemoryAssemblySpecificationRepository:
@@ -18,6 +18,8 @@ from julee.api.routers.documents import router
18
18
  from julee.domain.models.document import Document, DocumentStatus
19
19
  from julee.repositories.memory import MemoryDocumentRepository
20
20
 
21
+ pytestmark = pytest.mark.unit
22
+
21
23
 
22
24
  @pytest.fixture
23
25
  def memory_repo() -> MemoryDocumentRepository:
@@ -22,6 +22,8 @@ from julee.domain.models.knowledge_service_config import (
22
22
  ServiceApi,
23
23
  )
24
24
 
25
+ pytestmark = pytest.mark.unit
26
+
25
27
 
26
28
  @pytest.fixture
27
29
  def mock_repository() -> AsyncMock:
@@ -22,6 +22,8 @@ from julee.repositories.memory import (
22
22
  MemoryKnowledgeServiceQueryRepository,
23
23
  )
24
24
 
25
+ pytestmark = pytest.mark.unit
26
+
25
27
 
26
28
  @pytest.fixture
27
29
  def memory_repo() -> MemoryKnowledgeServiceQueryRepository:
@@ -17,6 +17,8 @@ from fastapi.testclient import TestClient
17
17
  from julee.api.responses import ServiceStatus
18
18
  from julee.api.routers.system import router
19
19
 
20
+ pytestmark = pytest.mark.unit
21
+
20
22
 
21
23
  @pytest.fixture
22
24
  def app_with_router() -> FastAPI:
@@ -16,6 +16,8 @@ from fastapi_pagination import add_pagination
16
16
  from julee.api.dependencies import get_temporal_client
17
17
  from julee.api.routers.workflows import router
18
18
 
19
+ pytestmark = pytest.mark.unit
20
+
19
21
 
20
22
  @pytest.fixture
21
23
  def mock_temporal_client() -> MagicMock:
@@ -25,6 +25,8 @@ from julee.repositories.memory.knowledge_service_config import (
25
25
  MemoryKnowledgeServiceConfigRepository,
26
26
  )
27
27
 
28
+ pytestmark = pytest.mark.unit
29
+
28
30
 
29
31
  @pytest.fixture
30
32
  def memory_repo() -> MemoryKnowledgeServiceQueryRepository:
@@ -16,6 +16,8 @@ from julee.api.dependencies import (
16
16
  get_startup_dependencies,
17
17
  )
18
18
 
19
+ pytestmark = pytest.mark.unit
20
+
19
21
 
20
22
  @pytest.fixture
21
23
  def mock_container() -> AsyncMock:
@@ -21,6 +21,8 @@ from julee.domain.models import (
21
21
  KnowledgeServiceQuery,
22
22
  )
23
23
 
24
+ pytestmark = pytest.mark.unit
25
+
24
26
 
25
27
  class TestCreateAssemblySpecificationRequest:
26
28
  """Test CreateAssemblySpecificationRequest model."""
@@ -12,8 +12,10 @@ The polling module includes:
12
12
  - Co-located tests and examples
13
13
 
14
14
  Example usage:
15
- from julee.contrib.polling import PollingConfig, HttpPollerService
16
- from julee.contrib.polling import PollingProtocol, PollingResult
15
+ from julee.contrib.polling.domain.models.polling_config import PollingConfig, PollingProtocol
16
+ from julee.contrib.polling.infrastructure.services.polling.http import HttpPollerService
17
+ from julee.contrib.polling.domain.services.poller import PollerService
18
+ from julee.contrib.polling.domain.models.polling_config import PollingResult
17
19
 
18
20
  # Configure polling
19
21
  config = PollingConfig(
@@ -26,22 +28,23 @@ Example usage:
26
28
  # Poll the endpoint
27
29
  service = HttpPollerService()
28
30
  result = await service.poll_endpoint(config)
31
+
32
+ Note: All imports must be explicit to avoid import chains that can pull
33
+ non-deterministic code into Temporal workflows. Import directly from
34
+ the specific modules you need rather than using this convenience module.
29
35
  """
30
36
 
31
- from .domain import PollerService, PollingConfig, PollingProtocol, PollingResult
32
- from .infrastructure import HttpPollerService, TemporalPollerService
33
- from .infrastructure.temporal import WorkflowPollerServiceProxy
34
-
35
- __all__ = [
36
- # Domain models
37
- "PollingConfig",
38
- "PollingProtocol",
39
- "PollingResult",
40
- # Domain services
41
- "PollerService",
42
- # Infrastructure implementations
43
- "HttpPollerService",
44
- # Temporal integration
45
- "TemporalPollerService",
46
- "WorkflowPollerServiceProxy",
47
- ]
37
+ # No re-exports to avoid import chains that pull non-deterministic code
38
+ # into Temporal workflows. Import from specific submodules instead:
39
+ #
40
+ # Domain:
41
+ # - from julee.contrib.polling.domain.models.polling_config import PollingConfig, PollingProtocol, PollingResult
42
+ # - from julee.contrib.polling.domain.services.poller import PollerService
43
+ #
44
+ # Infrastructure:
45
+ # - from julee.contrib.polling.infrastructure.services.polling.http import HttpPollerService
46
+ # - from julee.contrib.polling.infrastructure.temporal.manager import PollingManager
47
+ # - from julee.contrib.polling.infrastructure.temporal.proxies import WorkflowPollerServiceProxy
48
+ # - from julee.contrib.polling.infrastructure.temporal.activities import TemporalPollerService
49
+
50
+ __all__ = []
@@ -0,0 +1,17 @@
1
+ """
2
+ Application entry points for the polling contrib module.
3
+
4
+ This module contains the application-layer components that provide entry points
5
+ for the polling contrib module, including worker pipelines, API routes, and
6
+ CLI commands.
7
+
8
+ Following the ADR contrib module structure, this layer wires together domain
9
+ services and infrastructure implementations into runnable applications.
10
+
11
+ No re-exports to avoid import chains that pull non-deterministic code
12
+ into Temporal workflows. Import directly from specific modules:
13
+
14
+ - from julee.contrib.polling.apps.worker.pipelines import NewDataDetectionPipeline
15
+ """
16
+
17
+ __all__ = []
@@ -0,0 +1,17 @@
1
+ """
2
+ Worker applications for the polling contrib module.
3
+
4
+ This module contains worker-specific entry points for the polling contrib module,
5
+ including Temporal workflows (pipelines) that orchestrate polling operations
6
+ with durability guarantees.
7
+
8
+ The worker applications in this module can be registered with Temporal workers
9
+ to provide polling capabilities within workflow contexts.
10
+
11
+ No re-exports to avoid import chains that pull non-deterministic code
12
+ into Temporal workflows. Import directly from specific modules:
13
+
14
+ - from julee.contrib.polling.apps.worker.pipelines import NewDataDetectionPipeline
15
+ """
16
+
17
+ __all__ = []
@@ -0,0 +1,288 @@
1
+ """
2
+ Temporal workflows for polling operations in the Julee polling contrib module.
3
+
4
+ This module contains workflows that orchestrate polling operations with
5
+ Temporal's durability guarantees, providing retry logic, state management,
6
+ and reliable execution for endpoint polling and change detection.
7
+ """
8
+
9
+ import hashlib
10
+ import logging
11
+ from typing import Any
12
+
13
+ from temporalio import workflow
14
+
15
+ from julee.contrib.polling.domain.models.polling_config import PollingConfig
16
+ from julee.contrib.polling.infrastructure.temporal.proxies import (
17
+ WorkflowPollerServiceProxy,
18
+ )
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ @workflow.defn
24
+ class NewDataDetectionPipeline:
25
+ """
26
+ Temporal workflow for endpoint polling with new data detection.
27
+
28
+ This workflow:
29
+ 1. Polls an endpoint using the configured polling service
30
+ 2. Compares result with previous completion to detect changes
31
+ 3. Triggers downstream processing when new data is detected
32
+ 4. Returns completion result for next scheduled execution
33
+
34
+ The workflow uses Temporal's schedule last completion result feature
35
+ to automatically receive the previous execution's result for comparison.
36
+ """
37
+
38
+ def __init__(self) -> None:
39
+ self.current_step = "initialized"
40
+ self.endpoint_id: str | None = None
41
+ self.has_new_data: bool = False
42
+
43
+ @workflow.query
44
+ def get_current_step(self) -> str:
45
+ """Query method to get the current workflow step."""
46
+ return self.current_step
47
+
48
+ @workflow.query
49
+ def get_endpoint_id(self) -> str | None:
50
+ """Query method to get the endpoint ID being polled."""
51
+ return self.endpoint_id
52
+
53
+ @workflow.query
54
+ def get_has_new_data(self) -> bool:
55
+ """Query method to check if new data was detected."""
56
+ return self.has_new_data
57
+
58
+ async def trigger_downstream_pipeline(
59
+ self,
60
+ downstream_pipeline: str,
61
+ previous_data: bytes | None,
62
+ new_data: bytes,
63
+ ) -> bool:
64
+ """
65
+ Trigger downstream pipeline workflow.
66
+
67
+ Args:
68
+ downstream_pipeline: Name of the downstream workflow to trigger
69
+ previous_data: Previous content (None if first run)
70
+ new_data: New content that was detected
71
+
72
+ Returns:
73
+ True if successfully triggered, False otherwise
74
+ """
75
+ try:
76
+ # Start external workflow for downstream processing (fire-and-forget)
77
+ await workflow.start_child_workflow(
78
+ downstream_pipeline, # This would be the workflow class name
79
+ args=[previous_data, new_data],
80
+ id=f"downstream-{self.endpoint_id}-{workflow.info().workflow_id}",
81
+ task_queue="downstream-processing-queue",
82
+ )
83
+
84
+ workflow.logger.info(
85
+ "Downstream pipeline triggered successfully",
86
+ extra={
87
+ "endpoint_id": self.endpoint_id,
88
+ "downstream_pipeline": downstream_pipeline,
89
+ },
90
+ )
91
+ return True
92
+
93
+ except Exception as e:
94
+ workflow.logger.error(
95
+ "Failed to trigger downstream pipeline",
96
+ extra={
97
+ "endpoint_id": self.endpoint_id,
98
+ "downstream_pipeline": downstream_pipeline,
99
+ "error": str(e),
100
+ "error_type": type(e).__name__,
101
+ },
102
+ )
103
+ # Don't fail the polling workflow if downstream trigger fails
104
+ return False
105
+
106
+ @workflow.run
107
+ async def run(
108
+ self,
109
+ config: PollingConfig | dict[str, Any],
110
+ downstream_pipeline: str | None = None,
111
+ ) -> dict[str, Any]:
112
+ """
113
+ Execute the new data detection workflow.
114
+
115
+ Args:
116
+ config: Configuration for the polling operation (PollingConfig or dict from schedule)
117
+ downstream_pipeline: Optional pipeline to trigger when new data detected
118
+
119
+ Returns:
120
+ Completion result containing polling result and detection metadata
121
+
122
+ Raises:
123
+ RuntimeError: If polling or downstream processing fails after retries
124
+ """
125
+ # Convert dict to PollingConfig if needed (for schedule compatibility)
126
+ # Temporal schedules serialize arguments as dicts, not Pydantic models
127
+ if isinstance(config, dict):
128
+ config = PollingConfig.model_validate(config)
129
+
130
+ self.endpoint_id = config.endpoint_identifier
131
+
132
+ # Fetch previous completion result from Temporal
133
+ previous_completion = workflow.get_last_completion_result()
134
+
135
+ workflow.logger.info(
136
+ "Starting new data detection pipeline",
137
+ extra={
138
+ "endpoint_id": self.endpoint_id,
139
+ "polling_protocol": config.polling_protocol.value,
140
+ "has_previous_completion": previous_completion is not None,
141
+ "workflow_id": workflow.info().workflow_id,
142
+ "run_id": workflow.info().run_id,
143
+ },
144
+ )
145
+
146
+ self.current_step = "polling_endpoint"
147
+
148
+ try:
149
+ # Step 1: Poll the endpoint
150
+ polling_service = WorkflowPollerServiceProxy()
151
+ polling_result = await polling_service.poll_endpoint(config)
152
+
153
+ # Extract the timestamp from when polling actually happened
154
+ polled_at = polling_result.polled_at.isoformat()
155
+
156
+ workflow.logger.debug(
157
+ "Polling completed",
158
+ extra={
159
+ "endpoint_id": self.endpoint_id,
160
+ "polling_success": polling_result.success,
161
+ "content_length": len(polling_result.content),
162
+ },
163
+ )
164
+
165
+ self.current_step = "detecting_changes"
166
+
167
+ # Step 2: Detect new data using hash comparison
168
+ current_content = polling_result.content
169
+ current_hash = hashlib.sha256(current_content).hexdigest()
170
+
171
+ previous_hash = None
172
+ if previous_completion and "polling_result" in previous_completion:
173
+ previous_hash = previous_completion["polling_result"].get(
174
+ "content_hash"
175
+ )
176
+
177
+ has_new_data = previous_hash != current_hash
178
+ self.has_new_data = has_new_data
179
+
180
+ workflow.logger.info(
181
+ f"DEBUG: Change detection - has_new_data: {has_new_data}, "
182
+ f"is_first_run: {previous_hash is None}, "
183
+ f"current_hash: {current_hash[:8]}..., "
184
+ f"previous_hash: {previous_hash[:8] if previous_hash else 'None'}..."
185
+ )
186
+
187
+ # Step 3: Trigger downstream processing if new data detected
188
+ downstream_triggered = False
189
+ if has_new_data and downstream_pipeline:
190
+ self.current_step = "triggering_downstream"
191
+
192
+ workflow.logger.info(
193
+ "Triggering downstream pipeline",
194
+ extra={
195
+ "endpoint_id": self.endpoint_id,
196
+ "downstream_pipeline": downstream_pipeline,
197
+ "content_length": len(current_content),
198
+ },
199
+ )
200
+
201
+ # Get previous data for comparison
202
+ previous_data = None
203
+ if previous_completion and "polling_result" in previous_completion:
204
+ prev_content_str = previous_completion["polling_result"].get(
205
+ "content"
206
+ )
207
+ if prev_content_str:
208
+ try:
209
+ previous_data = prev_content_str.encode("utf-8")
210
+ except (UnicodeDecodeError, AttributeError) as e:
211
+ workflow.logger.error(
212
+ "Failed to decode previous content for downstream pipeline",
213
+ extra={
214
+ "endpoint_id": self.endpoint_id,
215
+ "error": str(e),
216
+ "error_type": type(e).__name__,
217
+ },
218
+ )
219
+ raise RuntimeError(
220
+ f"Previous content is corrupted or invalid: {e}"
221
+ )
222
+ elif previous_hash:
223
+ # We have previous run but no content - this is an error
224
+ workflow.logger.error(
225
+ "Previous content not available for downstream pipeline but previous hash exists",
226
+ extra={
227
+ "endpoint_id": self.endpoint_id,
228
+ "previous_hash": previous_hash,
229
+ },
230
+ )
231
+ raise RuntimeError(
232
+ "Previous content is missing from completion result but is required for downstream pipeline"
233
+ )
234
+
235
+ downstream_triggered = await self.trigger_downstream_pipeline(
236
+ downstream_pipeline,
237
+ previous_data,
238
+ current_content,
239
+ )
240
+
241
+ self.current_step = "completed"
242
+
243
+ # Step 4: Return completion result for next scheduled execution
244
+ completion_result = {
245
+ "polling_result": {
246
+ "success": polling_result.success,
247
+ "content_hash": current_hash,
248
+ "content": current_content.decode("utf-8", errors="ignore"),
249
+ "polled_at": polled_at,
250
+ "content_length": len(current_content),
251
+ },
252
+ "detection_result": {
253
+ "has_new_data": has_new_data,
254
+ "previous_hash": previous_hash,
255
+ "current_hash": current_hash,
256
+ },
257
+ "downstream_triggered": downstream_triggered,
258
+ "endpoint_id": self.endpoint_id,
259
+ "completed_at": workflow.now().isoformat(),
260
+ }
261
+
262
+ workflow.logger.info(
263
+ "New data detection pipeline completed successfully",
264
+ extra={
265
+ "endpoint_id": self.endpoint_id,
266
+ "has_new_data": has_new_data,
267
+ "downstream_triggered": downstream_triggered,
268
+ },
269
+ )
270
+
271
+ return completion_result
272
+
273
+ except Exception as e:
274
+ self.current_step = "failed"
275
+
276
+ workflow.logger.error(
277
+ "New data detection pipeline failed",
278
+ extra={
279
+ "endpoint_id": self.endpoint_id,
280
+ "error": str(e),
281
+ "error_type": type(e).__name__,
282
+ "current_step": self.current_step,
283
+ },
284
+ exc_info=True,
285
+ )
286
+
287
+ # Re-raise to let Temporal handle retry logic
288
+ raise
@@ -4,14 +4,12 @@ Domain layer for the polling contrib module.
4
4
  This module contains the core domain models, services, and business rules
5
5
  for the polling contrib module. It defines the fundamental concepts and
6
6
  protocols that govern polling operations.
7
- """
8
7
 
9
- from .models import PollingConfig, PollingProtocol, PollingResult
10
- from .services import PollerService
8
+ No re-exports to avoid import chains that pull non-deterministic code
9
+ into Temporal workflows. Import directly from specific modules:
10
+
11
+ - from julee.contrib.polling.domain.models.polling_config import PollingConfig, PollingProtocol, PollingResult
12
+ - from julee.contrib.polling.domain.services.poller import PollerService
13
+ """
11
14
 
12
- __all__ = [
13
- "PollingConfig",
14
- "PollingProtocol",
15
- "PollingResult",
16
- "PollerService",
17
- ]
15
+ __all__ = []
@@ -2,12 +2,11 @@
2
2
  Polling domain models.
3
3
 
4
4
  This module contains the core domain models for the polling contrib module.
5
- """
6
5
 
7
- from .polling_config import PollingConfig, PollingProtocol, PollingResult
6
+ No re-exports to avoid import chains that pull non-deterministic code
7
+ into Temporal workflows. Import directly from specific modules:
8
+
9
+ - from julee.contrib.polling.domain.models.polling_config import PollingConfig, PollingProtocol, PollingResult
10
+ """
8
11
 
9
- __all__ = [
10
- "PollingConfig",
11
- "PollingProtocol",
12
- "PollingResult",
13
- ]
12
+ __all__ = []
@@ -9,7 +9,7 @@ from datetime import datetime, timezone
9
9
  from enum import Enum
10
10
  from typing import Any
11
11
 
12
- from pydantic import BaseModel, Field
12
+ from pydantic import BaseModel, Field, field_validator
13
13
 
14
14
 
15
15
  class PollingProtocol(str, Enum):
@@ -37,3 +37,20 @@ class PollingResult(BaseModel):
37
37
  polled_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
38
38
  content_hash: str | None = None
39
39
  error_message: str | None = None
40
+
41
+ @field_validator("content", mode="before")
42
+ @classmethod
43
+ def validate_content(cls, v):
44
+ """Convert list of integers to bytes (for Temporal serialization compatibility)."""
45
+ if isinstance(v, list):
46
+ # Temporal may serialize bytes as list of integers
47
+ return bytes(v)
48
+ elif isinstance(v, str):
49
+ # Handle string input
50
+ return v.encode("utf-8")
51
+ elif isinstance(v, bytes):
52
+ return v
53
+ else:
54
+ raise ValueError(
55
+ f"Content must be bytes, string, or list of integers, got {type(v)}"
56
+ )
@@ -2,10 +2,11 @@
2
2
  Polling domain services.
3
3
 
4
4
  This module contains the service protocols for the polling contrib module.
5
- """
6
5
 
7
- from .poller import PollerService
6
+ No re-exports to avoid import chains that pull non-deterministic code
7
+ into Temporal workflows. Import directly from specific modules:
8
+
9
+ - from julee.contrib.polling.domain.services.poller import PollerService
10
+ """
8
11
 
9
- __all__ = [
10
- "PollerService",
11
- ]
12
+ __all__ = []
@@ -10,7 +10,7 @@ mechanisms and are created via factory functions.
10
10
 
11
11
  from typing import Protocol, runtime_checkable
12
12
 
13
- from ..models import PollingConfig, PollingResult
13
+ from ..models.polling_config import PollingConfig, PollingResult
14
14
 
15
15
 
16
16
  @runtime_checkable
@@ -3,13 +3,14 @@ Infrastructure layer for the polling contrib module.
3
3
 
4
4
  This module contains the concrete implementations of domain protocols
5
5
  and external system integrations for the polling contrib module.
6
- """
7
6
 
8
- from .services import HttpPollerService
9
- from .temporal import TemporalPollerService, WorkflowPollerServiceProxy
7
+ No re-exports to avoid import chains that pull non-deterministic code
8
+ into Temporal workflows. Import directly from specific modules:
9
+
10
+ - from julee.contrib.polling.infrastructure.services.polling.http import HttpPollerService
11
+ - from julee.contrib.polling.infrastructure.temporal.manager import PollingManager
12
+ - from julee.contrib.polling.infrastructure.temporal.proxies import WorkflowPollerServiceProxy
13
+ - from julee.contrib.polling.infrastructure.temporal.activities import TemporalPollerService
14
+ """
10
15
 
11
- __all__ = [
12
- "HttpPollerService",
13
- "TemporalPollerService",
14
- "WorkflowPollerServiceProxy",
15
- ]
16
+ __all__ = []
@@ -3,10 +3,11 @@ Infrastructure services for the polling contrib module.
3
3
 
4
4
  This module contains the concrete implementations of domain services
5
5
  for the polling contrib module.
6
- """
7
6
 
8
- from .polling import HttpPollerService
7
+ No re-exports to avoid import chains that pull non-deterministic code
8
+ into Temporal workflows. Import directly from specific modules:
9
+
10
+ - from julee.contrib.polling.infrastructure.services.polling.http import HttpPollerService
11
+ """
9
12
 
10
- __all__ = [
11
- "HttpPollerService",
12
- ]
13
+ __all__ = []
@@ -3,10 +3,11 @@ Polling infrastructure services.
3
3
 
4
4
  This module contains the concrete implementations of polling services
5
5
  for different protocols and mechanisms.
6
- """
7
6
 
8
- from .http import HttpPollerService
7
+ No re-exports to avoid import chains that pull non-deterministic code
8
+ into Temporal workflows. Import directly from specific modules:
9
+
10
+ - from julee.contrib.polling.infrastructure.services.polling.http import HttpPollerService
11
+ """
9
12
 
10
- __all__ = [
11
- "HttpPollerService",
12
- ]
13
+ __all__ = []
@@ -3,10 +3,11 @@ HTTP polling implementation.
3
3
 
4
4
  This module provides HTTP-specific polling functionality for the polling
5
5
  contrib module.
6
- """
7
6
 
8
- from .http_poller_service import HttpPollerService
7
+ No re-exports to avoid import chains that pull non-deterministic code
8
+ into Temporal workflows. Import directly from specific modules:
9
+
10
+ - from julee.contrib.polling.infrastructure.services.polling.http.http_poller_service import HttpPollerService
11
+ """
9
12
 
10
- __all__ = [
11
- "HttpPollerService",
12
- ]
13
+ __all__ = []
@@ -11,8 +11,11 @@ from typing import Any
11
11
 
12
12
  import httpx
13
13
 
14
- from julee.contrib.polling.domain.models import PollingConfig, PollingResult
15
- from julee.contrib.polling.domain.services import PollerService
14
+ from julee.contrib.polling.domain.models.polling_config import (
15
+ PollingConfig,
16
+ PollingResult,
17
+ )
18
+ from julee.contrib.polling.domain.services.poller import PollerService
16
19
 
17
20
 
18
21
  class HttpPollerService(PollerService):