tts-dictionary-interface 0.2.3__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. tts_dictionary_interface-0.4.0/.github/workflows/run_tests_on_pr.yml +14 -0
  2. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/.gitignore +3 -0
  3. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/PKG-INFO +2 -1
  4. tts_dictionary_interface-0.4.0/docs/agents/README.md +13 -0
  5. tts_dictionary_interface-0.4.0/docs/agents/check_docstring_coverage.py +166 -0
  6. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/docs/index.rst +1 -1
  7. tts_dictionary_interface-0.4.0/docs/narrative.rst +100 -0
  8. tts_dictionary_interface-0.4.0/docs/semantic_dictionary_cache_spec.md +47 -0
  9. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/pyproject.toml +5 -1
  10. tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/ait/__init__.py +3 -0
  11. tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/ait/loader.py +106 -0
  12. tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/base.py +338 -0
  13. tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/contracts.py +351 -0
  14. tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/fixtures/ait_yaml/demosat_includes/ccsds_header.yaml +8 -0
  15. tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/fixtures/ait_yaml/demosat_includes/demosat_channels.yaml +16 -0
  16. tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/fixtures/ait_yaml/demosat_includes/tlm.yaml +2 -0
  17. tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/fixtures/ait_yaml/generic_includes/mid/leaf/leaf.yaml +10 -0
  18. tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/fixtures/ait_yaml/generic_includes/mid/mid.yaml +8 -0
  19. tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/fixtures/ait_yaml/generic_includes/top.yaml +8 -0
  20. tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/test_ait_loader.py +62 -0
  21. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/src/tts_dictionary_interface/test/test_base.py +24 -11
  22. tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/test_cache.py +166 -0
  23. tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/test_contracts.py +290 -0
  24. tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/test_tree.py +331 -0
  25. tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/tree.py +216 -0
  26. tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/tree.py.bk +181 -0
  27. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/src/tts_dictionary_interface.egg-info/PKG-INFO +2 -1
  28. tts_dictionary_interface-0.4.0/src/tts_dictionary_interface.egg-info/SOURCES.txt +41 -0
  29. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/src/tts_dictionary_interface.egg-info/requires.txt +1 -0
  30. tts_dictionary_interface-0.2.3/src/tts_dictionary_interface/base.py +0 -244
  31. tts_dictionary_interface-0.2.3/src/tts_dictionary_interface.egg-info/SOURCES.txt +0 -21
  32. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/README.md +0 -0
  33. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/ci/sphinx_ignore_tags.txt +0 -0
  34. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/docs/_static/bugfix.css +0 -0
  35. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/docs/conf.py +0 -0
  36. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/docs/foo.rst +0 -0
  37. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/license.md +0 -0
  38. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/setup.cfg +0 -0
  39. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/setup.py +0 -0
  40. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/src/__init__.py +0 -0
  41. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/src/tts_dictionary_interface/__init__.py +0 -0
  42. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/src/tts_dictionary_interface/test/__init__.py +0 -0
  43. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/src/tts_dictionary_interface/test/conftest.py +0 -0
  44. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/src/tts_dictionary_interface.egg-info/dependency_links.txt +0 -0
  45. {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/src/tts_dictionary_interface.egg-info/top_level.txt +0 -0
@@ -0,0 +1,14 @@
1
+ name: PR Multi-Version Tests
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [ main ]
6
+ types: [opened, synchronize, reopened]
7
+
8
+ permissions:
9
+ pull-requests: write
10
+ contents: read
11
+
12
+ jobs:
13
+ call-shared-tests:
14
+ uses: NASA-JPL-Teamtools-Studio/tts_ci_cd/.github/workflows/run_tests_on_pr.yml@main
@@ -186,3 +186,6 @@ Thumbs.db
186
186
 
187
187
  #nfs temp files
188
188
  .nfs*
189
+
190
+ #other
191
+ tmp_dict_examples/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tts-dictionary-interface
3
- Version: 0.2.3
3
+ Version: 0.4.0
4
4
  Summary: Core code for developing Python AMPCS dictionary interfaces.
5
5
  Author: muszynsk
6
6
  Author-email: muszynsk <matt.muszynski@jpl.nasa.gov>
@@ -13,6 +13,7 @@ Description-Content-Type: text/markdown
13
13
  Requires-Dist: lxml
14
14
  Requires-Dist: pytest
15
15
  Requires-Dist: tomli
16
+ Requires-Dist: pyyaml
16
17
  Requires-Dist: tts-utilities<=1.0.0,>=0.2.2
17
18
  Dynamic: author
18
19
  Dynamic: requires-python
@@ -0,0 +1,13 @@
1
+ # Agent skills
2
+
3
+ Canonical engineering-skill conventions and the cross-repo dependency map live in
4
+ `tts_core/teamtools_documentation/docs/agents/`. This repo holds only the
5
+ per-repo instance data a skill needs locally.
6
+
7
+ - **Issue tracker:** GitHub Issues at `github.com/NASA-JPL-Teamtools-Studio/tts_dictionary_interface`
8
+ (host `github.com`, `-R NASA-JPL-Teamtools-Studio/tts_dictionary_interface`).
9
+ *If a skill needs more, see `teamtools_documentation/docs/agents/issue-tracker-github-ghe.md`.*
10
+ - **Adapts:** `unknown`
11
+ - **Triage labels:** default vocabulary (see `teamtools_documentation/docs/agents/triage-labels.md`)
12
+ - **Domain context:** repo-specific domain language is in this repo's `CONTEXT.md`.
13
+ - **Ownership:** public
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ AST-based docstring coverage check for tts_dictionary_interface.
4
+
5
+ Prints coverage percentage and exits 0 iff >=80% and no previously-missing
6
+ symbols remain undocumented.
7
+ """
8
+ import argparse
9
+ import ast
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ SKIP_DIRS = {"test", "build", ".venv", "__pycache__"}
14
+ SKIP_FILES = {"sandbox.py", "conftest.py"}
15
+
16
+ PREVIOUSLY_MISSING = {
17
+ "tts_dictionary_interface.base.SemanticDictionary.resolve_value",
18
+ "tts_dictionary_interface.tree.TreeSemanticDictionary.resolve",
19
+ "tts_dictionary_interface.contracts.ArgumentContract.name",
20
+ "tts_dictionary_interface.contracts.ArgumentContract.length",
21
+ "tts_dictionary_interface.contracts.ArgumentContract.units",
22
+ "tts_dictionary_interface.contracts.ArgumentContract.min",
23
+ "tts_dictionary_interface.contracts.ArgumentContract.max",
24
+ "tts_dictionary_interface.contracts.CommandContract.stem",
25
+ "tts_dictionary_interface.contracts.CommandContract.opcode",
26
+ "tts_dictionary_interface.contracts.CommandContract.opscat",
27
+ "tts_dictionary_interface.contracts.CommandContract.args",
28
+ "tts_dictionary_interface.contracts.ChannelContract.measurement_id",
29
+ "tts_dictionary_interface.contracts.ChannelContract.channel_id",
30
+ "tts_dictionary_interface.contracts.ChannelContract.channel_name",
31
+ "tts_dictionary_interface.contracts.ChannelContract.type",
32
+ "tts_dictionary_interface.contracts.ChannelContract.module",
33
+ "tts_dictionary_interface.contracts.EvrContract.message",
34
+ "tts_dictionary_interface.contracts.EvrContract.severity",
35
+ }
36
+
37
+ def is_public(name):
38
+ # Public if not private (single leading underscore). Dunder names are public.
39
+ return not (name.startswith("_") and not name.startswith("__"))
40
+
41
+ def module_name_from_path(py_path, src_root):
42
+ rel = py_path.relative_to(src_root)
43
+ parts = list(rel.with_suffix("").parts)
44
+ # strip leading src if present
45
+ if parts[0] == "src":
46
+ parts = parts[1:]
47
+ return ".".join(parts)
48
+
49
+ def collect_symbols(src_root):
50
+ symbols = [] # list of (fqname, node)
51
+ documented = set()
52
+ for py_path in src_root.rglob("*.py"):
53
+ # skip
54
+ if any(part in SKIP_DIRS for part in py_path.parts):
55
+ continue
56
+ if py_path.name in SKIP_FILES:
57
+ continue
58
+ # only src/tts_dictionary_interface
59
+ if "tts_dictionary_interface" not in str(py_path):
60
+ continue
61
+ try:
62
+ source = py_path.read_text(encoding="utf-8")
63
+ except Exception:
64
+ continue
65
+ try:
66
+ tree = ast.parse(source, filename=str(py_path))
67
+ except SyntaxError:
68
+ continue
69
+ mod_name = module_name_from_path(py_path, src_root.parent if src_root.name == "src" else src_root)
70
+ # fallback
71
+ # walk
72
+ for node in ast.walk(tree):
73
+ # We need parent context to avoid double counting
74
+ pass
75
+ # Simpler: manual walk with stack
76
+ symbols_info = []
77
+ for py_path in src_root.rglob("*.py"):
78
+ if any(part in SKIP_DIRS for part in py_path.parts):
79
+ continue
80
+ if py_path.name in SKIP_FILES:
81
+ continue
82
+ if "tts_dictionary_interface" not in str(py_path):
83
+ continue
84
+ try:
85
+ source = py_path.read_text(encoding="utf-8")
86
+ except Exception:
87
+ continue
88
+ try:
89
+ tree = ast.parse(source, filename=str(py_path))
90
+ except SyntaxError:
91
+ continue
92
+ mod_name = module_name_from_path(py_path, src_root.parent if src_root.name == "src" else src_root)
93
+ # process module level nodes
94
+ for node in tree.body:
95
+ if isinstance(node, ast.ClassDef) and is_public(node.name):
96
+ fq_class = f"{mod_name}.{node.name}"
97
+ symbols_info.append((fq_class, node))
98
+ # methods
99
+ for item in node.body:
100
+ if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)) and is_public(item.name):
101
+ fq = f"{fq_class}.{item.name}"
102
+ symbols_info.append((fq, item))
103
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and is_public(node.name):
104
+ fq = f"{mod_name}.{node.name}"
105
+ symbols_info.append((fq, node))
106
+ total = 0
107
+ documented_set = set()
108
+ for fq, node in symbols_info:
109
+ total += 1
110
+ if ast.get_docstring(node):
111
+ documented_set.add(fq)
112
+ return total, documented_set, symbols_info
113
+
114
+ def main():
115
+ parser = argparse.ArgumentParser()
116
+ parser.add_argument('--strict-sentences', action='store_true', help='Enforce summary sentence ends with period')
117
+ args = parser.parse_args()
118
+ # repo root is parent of src
119
+ repo_root = Path(__file__).resolve().parents[2]
120
+ src_root = repo_root / "src"
121
+ total, documented_set, symbols_info = collect_symbols(src_root)
122
+ documented_count = len(documented_set)
123
+ pct = (documented_count / total * 100) if total else 0.0
124
+ print(f"Coverage: {documented_count}/{total} = {pct:.1f}%")
125
+ # list symbols
126
+ print("\nSymbols:")
127
+ for fq, node in symbols_info:
128
+ status = "DOC" if fq in documented_set else "MISSING"
129
+ print(f" {status} {fq}")
130
+ # strict sentence check
131
+ sentence_failures = []
132
+ if args.strict_sentences:
133
+ for fq, node in symbols_info:
134
+ doc = ast.get_docstring(node)
135
+ if not doc:
136
+ continue
137
+ first_line = doc.strip().splitlines()[0].strip()
138
+ if not first_line.endswith('.'):
139
+ sentence_failures.append((fq, first_line))
140
+ # check previously missing
141
+ missing_prev = []
142
+ for sym in PREVIOUSLY_MISSING:
143
+ # if symbol not found, we consider it not applicable
144
+ found = any(fq == sym for fq, _ in symbols_info)
145
+ if not found:
146
+ continue
147
+ if sym not in documented_set:
148
+ missing_prev.append(sym)
149
+ if pct < 80:
150
+ print(f"\nFAIL: coverage {pct:.1f}% < 80%", file=sys.stderr)
151
+ sys.exit(1)
152
+ if sentence_failures:
153
+ print("\nFAIL: docstring summary sentences must end with period:", file=sys.stderr)
154
+ for fq, line in sentence_failures:
155
+ print(f" {fq}: {line}", file=sys.stderr)
156
+ sys.exit(1)
157
+ if missing_prev:
158
+ print("\nFAIL: previously missing symbols still undocumented:", file=sys.stderr)
159
+ for s in missing_prev:
160
+ print(f" {s}", file=sys.stderr)
161
+ sys.exit(1)
162
+ print("\nPASS")
163
+ sys.exit(0)
164
+
165
+ if __name__ == "__main__":
166
+ main()
@@ -5,7 +5,7 @@ Welcome to the Teamtools Starter Template documentation!
5
5
  :maxdepth: 2
6
6
  :caption: Contents:
7
7
 
8
- foo
8
+ narrative
9
9
 
10
10
  Indices and tables
11
11
  ==================
@@ -0,0 +1,100 @@
1
+ Semantic Dictionary Interface Narrative
2
+ =======================================
3
+
4
+ Purpose
5
+ -------
6
+
7
+ The ``tts_dictionary_interface`` package provides a format-agnostic interoperability layer for spacecraft dictionary data. It defines a small set of contracts and two core engine base classes – ``SemanticDictionary`` for XML/AMPCS dictionaries and ``TreeSemanticDictionary`` for dict/list-tree dictionaries such as AIT YAML – so that downstream Teamtools libraries can assume a common way to look up commands, telemetry channels, and event records.
8
+
9
+ The package does not implement mission-specific knowledge. Mission teams subclass the base classes and declare ``ITEM_PATHS``, ``ITEM_CLASSES``, ``ITEM_HUMAN_UNIQUE_IDS`` and ``ATTR_PATHS`` to map their dictionary schema onto the shared contracts.
10
+
11
+ Architecture Overview
12
+ --------------------
13
+
14
+ ASCII class inheritance diagram::
15
+
16
+ SemanticDictionary
17
+ |
18
+ +-- MissionXMLDictionary (e.g., DemoSat command dict)
19
+
20
+ TreeSemanticDictionary
21
+ |
22
+ +-- AitYamlDictionary
23
+ |
24
+ +-- MissionAITDictionary
25
+
26
+ Contracts
27
+ ArgumentContract
28
+ CommandContract
29
+ ChannelContract
30
+ EvrContract
31
+
32
+ Module Guide
33
+ ------------
34
+
35
+ ``tts_dictionary_interface.base``
36
+ ``SemanticDictionary`` – dictionary-like wrapper around lxml trees.
37
+ ``DictionaryAttributeError`` – raised when a single-valued attribute resolves to zero elements.
38
+
39
+ ``tts_dictionary_interface.tree``
40
+ ``TreeSemanticDictionary`` – analog of ``SemanticDictionary`` for dict/list trees.
41
+ ``select`` – path query helper used by ``TreeSemanticDictionary``.
42
+
43
+ ``tts_dictionary_interface.contracts``
44
+ Format-agnostic contracts for arguments, commands, channels, and EVRs. Concrete classes inherit from these contracts via multiple inheritance.
45
+
46
+ ``tts_dictionary_interface.ait.loader``
47
+ ``AitYamlDictionary`` – ``TreeSemanticDictionary`` subclass that loads AIT-flavored YAML with ``!include`` resolution.
48
+ ``IncludeLoader`` – PyYAML loader that resolves ``!include`` relative to the including file and ignores other custom tags.
49
+ ``load_yaml`` – public helper to load AIT YAML files.
50
+
51
+ Key Classes
52
+ -----------
53
+
54
+ ``SemanticDictionary``
55
+ Provides ``__getitem__``, ``__iter__``, ``__len__``, ``__contains__`` and dynamic attribute access via ``ATTR_XPATHS``. Subclasses configure ``ITEM_XPATHS``, ``ITEM_HUMAN_UNIQUE_IDS``, ``ITEM_CLASSES`` and ``ATTR_XPATHS``.
56
+
57
+ ``TreeSemanticDictionary``
58
+ Same concepts as ``SemanticDictionary`` but for dict/list trees. Uses a small path language ``a/b[k="v"]`` and supports cross-document resolution via the ``.document`` attribute.
59
+
60
+ ``ArgumentContract``, ``CommandContract``, ``ChannelContract``, ``EvrContract``
61
+ Define the required and optional attributes that mission classes must expose. Required attributes raise ``NotImplementedError`` if missing; optional attributes resolve to ``None``.
62
+
63
+ Extension Guide
64
+ ---------------
65
+
66
+ To add a mission-specific dictionary interface:
67
+
68
+ 1. Subclass ``SemanticDictionary`` for XML or ``TreeSemanticDictionary`` for YAML/JSON.
69
+ 2. Set ``DICTIONARY_MODULE`` and ``DICTIONARY_FILENAME`` for versioned package loading.
70
+ 3. Declare ``ITEM_PATHS`` / ``ITEM_XPATHS``, ``ITEM_HUMAN_UNIQUE_IDS``, ``ITEM_CLASSES`` and ``ATTR_PATHS`` / ``ATTR_XPATHS``.
71
+ 4. Make item classes inherit from the appropriate contract, e.g.:
72
+
73
+ .. code-block:: python
74
+
75
+ class CommandItem(SemanticDictionary, CommandContract):
76
+ pass
77
+
78
+ See ``demosat_dictionary_interface`` for a concrete example of how a mission plugs in.
79
+
80
+ Usage Example
81
+ -------------
82
+
83
+ Load a command dictionary and inspect a command:
84
+
85
+ .. code-block:: python
86
+
87
+ from my_mission_dict import CommandDictionary
88
+
89
+ cmd_dict = CommandDictionary('current')
90
+ cmd = cmd_dict['MY_CMD']
91
+ print(cmd.stem) # command stem
92
+ print(cmd.opcode) # command opcode
93
+ print([a.name for a in cmd.args]) # argument names
94
+
95
+ The same pattern works for ``TreeSemanticDictionary``-based YAML dictionaries via ``AitYamlDictionary``.
96
+
97
+ Relationships
98
+ -------------
99
+
100
+ The package uses ``tts_utilities.logger.create_logger`` for consistent logging. No other runtime dependencies are required beyond ``lxml``, ``PyYAML`` and ``importlib_resources`` for Python 3.6 compatibility.
@@ -0,0 +1,47 @@
1
+ # Implementation Spec: SemanticDictionary Node Caching
2
+ ## Overview
3
+ This specification outlines the implementation of a caching mechanism for `SemanticDictionary` to resolve performance issues caused by repeated XML parsing and XPath lookups.
4
+ ## Design Decisions
5
+ ### 1. Cache Scope and Lifecycle
6
+ - **Scope**: Session-scoped. Each `SemanticDictionary` instance will maintain its own private cache.
7
+ - **Lifecycle**: The cache is created upon instantiation and lives for the duration of the object.
8
+ ### 2. Cache Mechanism
9
+ - **Type**: A Python dictionary used as an LRU (Least Recently Used) cache.
10
+ - **Capacity**: Bounded to 100 entries to prevent unbounded memory growth.
11
+ - **Concurrency**: Non-thread-safe (to minimize overhead, per developer requirement).
12
+ ### 3. Keying Strategy
13
+ To avoid collisions between different lookup methods, the cache will use a composite key: `(lookup_type, lookup_value)`.
14
+ | Method | `lookup_type` | `lookup_value` |
15
+ |---|---|---|
16
+ | `__getitem__` | `'getitem'` | The item ID (e.g., command stem) |
17
+ | `__contains__` | `'getitem'` | The key being checked |
18
+ | `__getattr__` | `'getattr'` | The attribute name |
19
+ | `__iter__` | `'getitem'` | The item ID (from `ITEM_HUMAN_UNIQUE_IDS`) |
20
+ ### 4. Detailed Implementation Plan
21
+ #### A. Initialization
22
+ Add `self._cache = collections.OrderedDict()` to `SemanticDictionary.__init__`.
23
+ #### B. `__getitem__` and `__contains__`
24
+ - Check `self._cache.get(('getitem', item))`.
25
+ - If present, return/confirm.
26
+ - If absent:
27
+ - Perform the existing XPath logic.
28
+ - If a result is found, wrap it in the appropriate `ITEM_CLASS`.
29
+ - Add to `self._cache` with key `('getitem', item)`.
30
+ - Maintain LRU order by moving the key to the end if it exists, or removing the oldest if size > 100.
31
+ #### C. `__getattr__`
32
+ - Check `self._cache.get(('getattr', attr))`.
33
+ - If present, return.
34
+ - If absent:
35
+ - Perform existing logic.
36
+ - Store result in `self._cache` with key `('getattr', attr)`.
37
+ - Maintain LRU order.
38
+ #### D. `__iter__` and `__len__`
39
+ - `__iter__` will implement "lazy population":
40
+ - Iterate through `ITEM_XPATHS` and `ITEM_HUMAN_UNIQUE_IDS`.
41
+ - For each item, perform the wrap and store in `self._cache` using `('getitem', identifier)`.
42
+ - `__len__` will return the count of items in the `'getitem'` cache after the first iteration has completed, or a cached count.
43
+ #### E. Invalidation
44
+ - No explicit `refresh()` method will be implemented at this time, as per developer request.
45
+ ## Verification Plan
46
+ 1. **Profiling**: Run `profile_test.py` to confirm the speedup factor (expected >10x for subsequent accesses).
47
+ 2. **Correctness**: Ensure `__getitem__`, `__getattr__`, `__contains__`, and `__iter__` all return correct, consistent data.
@@ -15,6 +15,7 @@ dependencies = [
15
15
  "lxml",
16
16
  "pytest",
17
17
  "tomli",
18
+ "pyyaml",
18
19
  "tts-utilities>=0.2.2,<=1.0.0"
19
20
  ]
20
21
  dynamic = ["version"]
@@ -37,4 +38,7 @@ requires = [
37
38
  "tts-utilities>=0.2.1,<1.0.0",
38
39
  "setuptools-scm"
39
40
  ]
40
- build-backend = "setuptools.build_meta"
41
+ build-backend = "setuptools.build_meta"
42
+
43
+ [tool.tts-ctx]
44
+ type = "core"
@@ -0,0 +1,3 @@
1
+ from tts_dictionary_interface.ait.loader import AitYamlDictionary, load_yaml
2
+
3
+ __all__ = ['load_yaml', 'AitYamlDictionary']
@@ -0,0 +1,106 @@
1
+ """
2
+ PyYAML loading support for AIT-core-style dictionaries (`!Packet`/`!Field`/
3
+ `!Command`/`!Argument`/`!EVR`, and similar mission-specific tags such as
4
+ `!Fixed`).
5
+
6
+ Two responsibilities, per design decisions made while grilling issue #21:
7
+
8
+ 1. Every AIT-core tag is treated as a no-op: a tagged mapping/sequence/
9
+ scalar loads as a plain dict/list/str, ignoring AIT-core's own Python
10
+ tag semantics entirely. This keeps the engine free of any dependency
11
+ on the `ait-core` package.
12
+
13
+ 2. `!include <relative-path>` is a real, resolved directive: the
14
+ referenced file is loaded (relative to the *including* file's own
15
+ directory, never a shared root or the process's cwd) and spliced in
16
+ as if its contents had been written inline. This mirrors the real
17
+ oco3mos dictionaries, where each FSW-version directory's `tlm.yaml` is
18
+ just a manifest of `!include` pointers at its own sibling files.
19
+ Includes may nest to any depth -- resolution is always relative to
20
+ whichever file directly contains the `!include`.
21
+ """
22
+ import os
23
+
24
+ import yaml
25
+
26
+ from tts_dictionary_interface.tree import TreeSemanticDictionary, _flatten
27
+
28
+
29
+ class IncludeLoader(yaml.SafeLoader):
30
+ """
31
+ YAML loader that resolves !include relative to the including file.
32
+
33
+ It treats every other custom tag as a no-op, loading nodes as plain
34
+ dict/list/scalar values.
35
+ """
36
+
37
+ def __init__(self, stream):
38
+ """
39
+ Initialize the IncludeLoader with the current file's directory.
40
+
41
+ The root directory is used to resolve `!include` paths relative
42
+ to the including file.
43
+ """
44
+ stream_name = getattr(stream, 'name', None)
45
+ self._root_dir = os.path.dirname(stream_name) if stream_name else '.'
46
+ super().__init__(stream)
47
+
48
+
49
+ def _construct_include(loader, node):
50
+ relative_path = loader.construct_scalar(node)
51
+ path = os.path.join(loader._root_dir, relative_path)
52
+ with open(path, 'r') as f:
53
+ return yaml.load(f, IncludeLoader)
54
+
55
+
56
+ def _construct_ignore_tag(loader, node):
57
+ """
58
+ Load any tagged node as if it were untagged, per its underlying YAML
59
+ kind (mapping/sequence/scalar) -- ignores the tag itself.
60
+ """
61
+ if isinstance(node, yaml.MappingNode):
62
+ return loader.construct_mapping(node, deep=True)
63
+ if isinstance(node, yaml.SequenceNode):
64
+ return loader.construct_sequence(node, deep=True)
65
+ return loader.construct_scalar(node)
66
+
67
+
68
+ IncludeLoader.add_constructor('!include', _construct_include)
69
+ # `None` registers a fallback constructor for any tag with no exact or
70
+ # multi-constructor match -- i.e. every AIT-core tag except `!include`.
71
+ IncludeLoader.add_constructor(None, _construct_ignore_tag)
72
+
73
+
74
+ def load_yaml(path):
75
+ """
76
+ Load an AIT-core-style YAML dictionary file.
77
+
78
+ Includes are resolved recursively relative to each including file's
79
+ own directory, and all other custom tags are ignored.
80
+ """
81
+ with open(path, 'r') as f:
82
+ return yaml.load(f, IncludeLoader)
83
+
84
+
85
+ class AitYamlDictionary(TreeSemanticDictionary):
86
+ """
87
+ TreeSemanticDictionary for AIT-flavored YAML files.
88
+
89
+ It resolves !include directives via load_yaml and flattens the result
90
+ into a single flat list, so documents assembled from nested includes
91
+ behave like one flat list of items.
92
+
93
+ Concrete subclasses set DICTIONARY_FILENAME so that source arguments
94
+ resolved against DICTIONARY_MODULE know which file to load.
95
+ """
96
+
97
+ @classmethod
98
+ def _load_file(cls, path):
99
+ if os.path.isdir(path):
100
+ if not cls.DICTIONARY_FILENAME:
101
+ raise ValueError(
102
+ f"{cls.__name__} must define DICTIONARY_FILENAME to load from a directory."
103
+ )
104
+ path = os.path.join(path, cls.DICTIONARY_FILENAME)
105
+ loaded = load_yaml(path)
106
+ return _flatten(loaded) if isinstance(loaded, list) else loaded