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.
- tts_dictionary_interface-0.4.0/.github/workflows/run_tests_on_pr.yml +14 -0
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/.gitignore +3 -0
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/PKG-INFO +2 -1
- tts_dictionary_interface-0.4.0/docs/agents/README.md +13 -0
- tts_dictionary_interface-0.4.0/docs/agents/check_docstring_coverage.py +166 -0
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/docs/index.rst +1 -1
- tts_dictionary_interface-0.4.0/docs/narrative.rst +100 -0
- tts_dictionary_interface-0.4.0/docs/semantic_dictionary_cache_spec.md +47 -0
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/pyproject.toml +5 -1
- tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/ait/__init__.py +3 -0
- tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/ait/loader.py +106 -0
- tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/base.py +338 -0
- tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/contracts.py +351 -0
- tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/fixtures/ait_yaml/demosat_includes/ccsds_header.yaml +8 -0
- tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/fixtures/ait_yaml/demosat_includes/demosat_channels.yaml +16 -0
- tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/fixtures/ait_yaml/demosat_includes/tlm.yaml +2 -0
- tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/fixtures/ait_yaml/generic_includes/mid/leaf/leaf.yaml +10 -0
- tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/fixtures/ait_yaml/generic_includes/mid/mid.yaml +8 -0
- tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/fixtures/ait_yaml/generic_includes/top.yaml +8 -0
- tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/test_ait_loader.py +62 -0
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/src/tts_dictionary_interface/test/test_base.py +24 -11
- tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/test_cache.py +166 -0
- tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/test_contracts.py +290 -0
- tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/test/test_tree.py +331 -0
- tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/tree.py +216 -0
- tts_dictionary_interface-0.4.0/src/tts_dictionary_interface/tree.py.bk +181 -0
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/src/tts_dictionary_interface.egg-info/PKG-INFO +2 -1
- tts_dictionary_interface-0.4.0/src/tts_dictionary_interface.egg-info/SOURCES.txt +41 -0
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/src/tts_dictionary_interface.egg-info/requires.txt +1 -0
- tts_dictionary_interface-0.2.3/src/tts_dictionary_interface/base.py +0 -244
- tts_dictionary_interface-0.2.3/src/tts_dictionary_interface.egg-info/SOURCES.txt +0 -21
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/README.md +0 -0
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/ci/sphinx_ignore_tags.txt +0 -0
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/docs/_static/bugfix.css +0 -0
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/docs/conf.py +0 -0
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/docs/foo.rst +0 -0
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/license.md +0 -0
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/setup.cfg +0 -0
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/setup.py +0 -0
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/src/__init__.py +0 -0
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/src/tts_dictionary_interface/__init__.py +0 -0
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/src/tts_dictionary_interface/test/__init__.py +0 -0
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/src/tts_dictionary_interface/test/conftest.py +0 -0
- {tts_dictionary_interface-0.2.3 → tts_dictionary_interface-0.4.0}/src/tts_dictionary_interface.egg-info/dependency_links.txt +0 -0
- {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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tts-dictionary-interface
|
|
3
|
-
Version: 0.
|
|
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()
|
|
@@ -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,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
|