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
@@ -0,0 +1,42 @@
1
+ """Env-purge-doc event handler for sphinx_hcd.
2
+
3
+ Clears document-specific state when a document is re-read.
4
+ """
5
+
6
+ from ..directives import (
7
+ clear_accelerator_state,
8
+ clear_epic_state,
9
+ clear_journey_state,
10
+ )
11
+
12
+
13
+ def on_env_purge_doc(app, env, docname):
14
+ """Clear state when a document is re-read.
15
+
16
+ This handler runs when a document needs to be re-read (incremental build).
17
+ It clears any state associated with that document.
18
+
19
+ Args:
20
+ app: Sphinx application instance
21
+ env: Sphinx build environment
22
+ docname: The document name being purged
23
+ """
24
+ # Clear epic state for this document
25
+ clear_epic_state(app, env, docname)
26
+
27
+ # Clear journey state for this document
28
+ clear_journey_state(app, env, docname)
29
+
30
+ # Clear accelerator state for this document
31
+ clear_accelerator_state(app, env, docname)
32
+
33
+ # Clear documented apps tracker
34
+ if hasattr(env, "documented_apps") and docname in env.documented_apps:
35
+ env.documented_apps.discard(docname)
36
+
37
+ # Clear documented integrations tracker
38
+ if (
39
+ hasattr(env, "documented_integrations")
40
+ and docname in env.documented_integrations
41
+ ):
42
+ env.documented_integrations.discard(docname)
@@ -0,0 +1,139 @@
1
+ """Sphinx initialization handlers for HCD context.
2
+
3
+ Handles builder-inited event to set up the HCDContext and populate
4
+ repositories with data that doesn't change during the build.
5
+ """
6
+
7
+ import logging
8
+
9
+ from ..config import get_config
10
+ from ..parsers import (
11
+ scan_app_manifests,
12
+ scan_bounded_contexts,
13
+ scan_feature_directory,
14
+ scan_integration_manifests,
15
+ )
16
+ from .context import HCDContext, set_hcd_context
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def initialize_hcd_context(app) -> None:
22
+ """Initialize HCDContext at builder-inited.
23
+
24
+ Creates the context and populates repositories with data that is
25
+ static during the build:
26
+ - Stories (from .feature files)
27
+ - Apps (from app.yaml manifests)
28
+ - Integrations (from integration.yaml manifests)
29
+ - Code info (from src/ introspection)
30
+
31
+ Journeys, epics, and accelerators are populated during doctree
32
+ processing as they're defined in RST files.
33
+
34
+ Args:
35
+ app: Sphinx application object
36
+ """
37
+ context = HCDContext()
38
+ set_hcd_context(app, context)
39
+
40
+ config = get_config()
41
+
42
+ # Load stories from feature files
43
+ _load_stories(context, config)
44
+
45
+ # Load apps from manifests
46
+ _load_apps(context, config)
47
+
48
+ # Load integrations from manifests
49
+ _load_integrations(context, config)
50
+
51
+ # Load code info from src/ introspection
52
+ _load_code_info(context, config)
53
+
54
+ logger.info("HCDContext initialized")
55
+
56
+
57
+ def _load_stories(context: HCDContext, config) -> None:
58
+ """Load stories from feature files into the repository."""
59
+ features_dir = config.get_path("feature_files")
60
+ if not features_dir.exists():
61
+ logger.info(f"Features directory not found: {features_dir}")
62
+ return
63
+
64
+ stories = scan_feature_directory(features_dir, config.project_root)
65
+ for story in stories:
66
+ context.story_repo.save(story)
67
+
68
+ logger.info(f"Loaded {len(stories)} stories from feature files")
69
+
70
+
71
+ def _load_apps(context: HCDContext, config) -> None:
72
+ """Load apps from manifest files into the repository."""
73
+ apps_dir = config.get_path("app_manifests")
74
+ if not apps_dir.exists():
75
+ logger.info(f"Applications directory not found: {apps_dir}")
76
+ return
77
+
78
+ apps = scan_app_manifests(apps_dir)
79
+ for app in apps:
80
+ context.app_repo.save(app)
81
+
82
+ logger.info(f"Loaded {len(apps)} apps from manifests")
83
+
84
+
85
+ def _load_integrations(context: HCDContext, config) -> None:
86
+ """Load integrations from manifest files into the repository."""
87
+ integrations_dir = config.get_path("integration_manifests")
88
+ if not integrations_dir.exists():
89
+ logger.info(f"Integrations directory not found: {integrations_dir}")
90
+ return
91
+
92
+ integrations = scan_integration_manifests(integrations_dir)
93
+ for integration in integrations:
94
+ context.integration_repo.save(integration)
95
+
96
+ logger.info(f"Loaded {len(integrations)} integrations from manifests")
97
+
98
+
99
+ def _load_code_info(context: HCDContext, config) -> None:
100
+ """Load code info from src/ introspection into the repository."""
101
+ src_dir = config.get_path("bounded_contexts")
102
+ if not src_dir.exists():
103
+ logger.info(f"Source directory not found: {src_dir}")
104
+ return
105
+
106
+ contexts = scan_bounded_contexts(src_dir)
107
+ for code_info in contexts:
108
+ context.code_info_repo.save(code_info)
109
+
110
+ logger.info(f"Loaded {len(contexts)} bounded contexts from source")
111
+
112
+
113
+ def purge_doc_from_context(app, env, docname: str) -> None:
114
+ """Purge entities from a document when it's being re-read.
115
+
116
+ Called during env-purge-doc event for incremental builds.
117
+
118
+ Args:
119
+ app: Sphinx application object
120
+ env: Sphinx environment
121
+ docname: Document being purged
122
+ """
123
+ from .context import get_hcd_context
124
+
125
+ try:
126
+ context = get_hcd_context(app)
127
+ results = context.clear_by_docname(docname)
128
+
129
+ total = sum(results.values())
130
+ if total > 0:
131
+ logger.debug(
132
+ f"Purged from {docname}: "
133
+ f"{results.get('journeys', 0)} journeys, "
134
+ f"{results.get('epics', 0)} epics, "
135
+ f"{results.get('accelerators', 0)} accelerators"
136
+ )
137
+ except AttributeError:
138
+ # Context not initialized yet - this is fine during startup
139
+ pass
@@ -0,0 +1,9 @@
1
+ """Tests for sphinx_hcd.
2
+
3
+ Organized by layer:
4
+ - domain/: Domain model and use case tests
5
+ - repositories/: Repository implementation tests
6
+ - parsers/: Parser tests
7
+ - sphinx/: Sphinx adapter and directive tests
8
+ - integration/: Full Sphinx build tests
9
+ """
@@ -0,0 +1,6 @@
1
+ """Pytest configuration and fixtures for sphinx_hcd tests."""
2
+
3
+ import pytest
4
+
5
+ # Mark all tests in this directory as unit tests by default
6
+ pytestmark = pytest.mark.unit
@@ -0,0 +1 @@
1
+ """Domain layer tests."""
@@ -0,0 +1 @@
1
+ """Domain model tests."""
@@ -0,0 +1,266 @@
1
+ """Tests for Accelerator domain model."""
2
+
3
+ import pytest
4
+ from pydantic import ValidationError
5
+
6
+ from julee.docs.sphinx_hcd.domain.models.accelerator import (
7
+ Accelerator,
8
+ IntegrationReference,
9
+ )
10
+
11
+
12
+ class TestIntegrationReference:
13
+ """Test IntegrationReference model."""
14
+
15
+ def test_create_with_slug_only(self) -> None:
16
+ """Test creating with just slug."""
17
+ ref = IntegrationReference(slug="pilot-data")
18
+ assert ref.slug == "pilot-data"
19
+ assert ref.description == ""
20
+
21
+ def test_create_with_description(self) -> None:
22
+ """Test creating with description."""
23
+ ref = IntegrationReference(
24
+ slug="pilot-data",
25
+ description="Scheme documentation, standards materials",
26
+ )
27
+ assert ref.slug == "pilot-data"
28
+ assert ref.description == "Scheme documentation, standards materials"
29
+
30
+ def test_empty_slug_raises_error(self) -> None:
31
+ """Test that empty slug raises validation error."""
32
+ with pytest.raises(ValidationError, match="slug cannot be empty"):
33
+ IntegrationReference(slug="")
34
+
35
+ def test_from_dict_complete(self) -> None:
36
+ """Test from_dict with full dict."""
37
+ ref = IntegrationReference.from_dict(
38
+ {
39
+ "slug": "pilot-data",
40
+ "description": "Test description",
41
+ }
42
+ )
43
+ assert ref.slug == "pilot-data"
44
+ assert ref.description == "Test description"
45
+
46
+ def test_from_dict_string(self) -> None:
47
+ """Test from_dict with plain string."""
48
+ ref = IntegrationReference.from_dict("pilot-data")
49
+ assert ref.slug == "pilot-data"
50
+ assert ref.description == ""
51
+
52
+ def test_from_dict_minimal(self) -> None:
53
+ """Test from_dict with minimal dict."""
54
+ ref = IntegrationReference.from_dict({"slug": "pilot-data"})
55
+ assert ref.slug == "pilot-data"
56
+ assert ref.description == ""
57
+
58
+
59
+ class TestAcceleratorCreation:
60
+ """Test Accelerator model creation and validation."""
61
+
62
+ def test_create_accelerator_minimal(self) -> None:
63
+ """Test creating an accelerator with minimum fields."""
64
+ accel = Accelerator(slug="vocabulary")
65
+ assert accel.slug == "vocabulary"
66
+ assert accel.status == ""
67
+ assert accel.milestone is None
68
+ assert accel.acceptance is None
69
+ assert accel.objective == ""
70
+ assert accel.sources_from == []
71
+ assert accel.feeds_into == []
72
+ assert accel.publishes_to == []
73
+ assert accel.depends_on == []
74
+ assert accel.docname == ""
75
+
76
+ def test_create_accelerator_complete(self) -> None:
77
+ """Test creating an accelerator with all fields."""
78
+ accel = Accelerator(
79
+ slug="vocabulary",
80
+ status="alpha",
81
+ milestone="2 (Nov 2025)",
82
+ acceptance="Reference environment deployed and accepted.",
83
+ objective="Accelerate the creation of Sustainable Vocabulary Catalogs.",
84
+ sources_from=[
85
+ IntegrationReference(
86
+ slug="pilot-data-collection",
87
+ description="Scheme documentation, standards materials",
88
+ ),
89
+ ],
90
+ feeds_into=["traceability", "conformity"],
91
+ publishes_to=[
92
+ IntegrationReference(
93
+ slug="reference-implementation",
94
+ description="SVC artefacts",
95
+ ),
96
+ ],
97
+ depends_on=["core-infrastructure"],
98
+ docname="accelerators/vocabulary",
99
+ )
100
+
101
+ assert accel.slug == "vocabulary"
102
+ assert accel.status == "alpha"
103
+ assert accel.milestone == "2 (Nov 2025)"
104
+ assert len(accel.sources_from) == 1
105
+ assert accel.sources_from[0].slug == "pilot-data-collection"
106
+ assert len(accel.feeds_into) == 2
107
+ assert len(accel.publishes_to) == 1
108
+
109
+ def test_empty_slug_raises_error(self) -> None:
110
+ """Test that empty slug raises validation error."""
111
+ with pytest.raises(ValidationError, match="slug cannot be empty"):
112
+ Accelerator(slug="")
113
+
114
+ def test_whitespace_slug_raises_error(self) -> None:
115
+ """Test that whitespace-only slug raises validation error."""
116
+ with pytest.raises(ValidationError, match="slug cannot be empty"):
117
+ Accelerator(slug=" ")
118
+
119
+ def test_slug_stripped(self) -> None:
120
+ """Test that slug is stripped of whitespace."""
121
+ accel = Accelerator(slug=" vocabulary ")
122
+ assert accel.slug == "vocabulary"
123
+
124
+
125
+ class TestAcceleratorProperties:
126
+ """Test Accelerator properties."""
127
+
128
+ def test_display_title(self) -> None:
129
+ """Test display_title property."""
130
+ accel = Accelerator(slug="vocabulary")
131
+ assert accel.display_title == "Vocabulary"
132
+
133
+ def test_display_title_multiple_words(self) -> None:
134
+ """Test display_title with hyphens."""
135
+ accel = Accelerator(slug="core-infrastructure")
136
+ assert accel.display_title == "Core Infrastructure"
137
+
138
+ def test_status_normalized(self) -> None:
139
+ """Test status_normalized property."""
140
+ accel = Accelerator(slug="test", status="Alpha")
141
+ assert accel.status_normalized == "alpha"
142
+
143
+ def test_status_normalized_empty(self) -> None:
144
+ """Test status_normalized with empty status."""
145
+ accel = Accelerator(slug="test")
146
+ assert accel.status_normalized == ""
147
+
148
+
149
+ class TestAcceleratorDependencies:
150
+ """Test Accelerator dependency methods."""
151
+
152
+ @pytest.fixture
153
+ def sample_accelerator(self) -> Accelerator:
154
+ """Create a sample accelerator for testing."""
155
+ return Accelerator(
156
+ slug="vocabulary",
157
+ sources_from=[
158
+ IntegrationReference(slug="pilot-data", description="Pilot data"),
159
+ IntegrationReference(slug="standards", description="Standards"),
160
+ ],
161
+ publishes_to=[
162
+ IntegrationReference(slug="reference-impl", description="SVC"),
163
+ ],
164
+ feeds_into=["traceability", "conformity"],
165
+ depends_on=["core-infrastructure"],
166
+ )
167
+
168
+ def test_has_integration_dependency_sources(
169
+ self, sample_accelerator: Accelerator
170
+ ) -> None:
171
+ """Test checking sources_from dependency."""
172
+ assert sample_accelerator.has_integration_dependency("pilot-data") is True
173
+ assert sample_accelerator.has_integration_dependency("standards") is True
174
+
175
+ def test_has_integration_dependency_publishes(
176
+ self, sample_accelerator: Accelerator
177
+ ) -> None:
178
+ """Test checking publishes_to dependency."""
179
+ assert sample_accelerator.has_integration_dependency("reference-impl") is True
180
+
181
+ def test_has_integration_dependency_no_match(
182
+ self, sample_accelerator: Accelerator
183
+ ) -> None:
184
+ """Test checking nonexistent dependency."""
185
+ assert sample_accelerator.has_integration_dependency("unknown") is False
186
+
187
+ def test_has_accelerator_dependency_depends(
188
+ self, sample_accelerator: Accelerator
189
+ ) -> None:
190
+ """Test checking depends_on dependency."""
191
+ assert (
192
+ sample_accelerator.has_accelerator_dependency("core-infrastructure") is True
193
+ )
194
+
195
+ def test_has_accelerator_dependency_feeds(
196
+ self, sample_accelerator: Accelerator
197
+ ) -> None:
198
+ """Test checking feeds_into dependency."""
199
+ assert sample_accelerator.has_accelerator_dependency("traceability") is True
200
+ assert sample_accelerator.has_accelerator_dependency("conformity") is True
201
+
202
+ def test_has_accelerator_dependency_no_match(
203
+ self, sample_accelerator: Accelerator
204
+ ) -> None:
205
+ """Test checking nonexistent accelerator dependency."""
206
+ assert sample_accelerator.has_accelerator_dependency("unknown") is False
207
+
208
+ def test_get_sources_from_slugs(self, sample_accelerator: Accelerator) -> None:
209
+ """Test getting source integration slugs."""
210
+ slugs = sample_accelerator.get_sources_from_slugs()
211
+ assert slugs == ["pilot-data", "standards"]
212
+
213
+ def test_get_publishes_to_slugs(self, sample_accelerator: Accelerator) -> None:
214
+ """Test getting publish integration slugs."""
215
+ slugs = sample_accelerator.get_publishes_to_slugs()
216
+ assert slugs == ["reference-impl"]
217
+
218
+ def test_get_integration_description_sources(
219
+ self, sample_accelerator: Accelerator
220
+ ) -> None:
221
+ """Test getting description from sources_from."""
222
+ desc = sample_accelerator.get_integration_description(
223
+ "pilot-data", "sources_from"
224
+ )
225
+ assert desc == "Pilot data"
226
+
227
+ def test_get_integration_description_publishes(
228
+ self, sample_accelerator: Accelerator
229
+ ) -> None:
230
+ """Test getting description from publishes_to."""
231
+ desc = sample_accelerator.get_integration_description(
232
+ "reference-impl", "publishes_to"
233
+ )
234
+ assert desc == "SVC"
235
+
236
+ def test_get_integration_description_not_found(
237
+ self, sample_accelerator: Accelerator
238
+ ) -> None:
239
+ """Test getting description for nonexistent integration."""
240
+ desc = sample_accelerator.get_integration_description("unknown", "sources_from")
241
+ assert desc is None
242
+
243
+
244
+ class TestAcceleratorSerialization:
245
+ """Test Accelerator serialization."""
246
+
247
+ def test_accelerator_to_dict(self) -> None:
248
+ """Test accelerator can be serialized to dict."""
249
+ accel = Accelerator(
250
+ slug="test",
251
+ status="alpha",
252
+ sources_from=[IntegrationReference(slug="pilot", description="Data")],
253
+ )
254
+
255
+ data = accel.model_dump()
256
+ assert data["slug"] == "test"
257
+ assert data["status"] == "alpha"
258
+ assert len(data["sources_from"]) == 1
259
+ assert data["sources_from"][0]["slug"] == "pilot"
260
+
261
+ def test_accelerator_to_json(self) -> None:
262
+ """Test accelerator can be serialized to JSON."""
263
+ accel = Accelerator(slug="test", status="alpha")
264
+ json_str = accel.model_dump_json()
265
+ assert '"slug":"test"' in json_str
266
+ assert '"status":"alpha"' in json_str