From 93f9a37ad7dc01858f53b58cc37a5cf091830f56 Mon Sep 17 00:00:00 2001 From: Brian Sipos Date: Fri, 31 Jan 2025 11:24:42 -0500 Subject: [PATCH 1/3] Initial package updates and test updates --- src/ace/adm_set.py | 2 +- src/ace/adm_yang.py | 48 ++- src/ace/ari.py | 43 +- src/ace/ari_cbor.py | 16 +- src/ace/ari_text/encode.py | 16 +- src/ace/ari_text/parsemod.py | 52 ++- src/ace/lookup.py | 35 +- src/ace/models.py | 32 +- src/ace/nickname.py | 2 +- src/ace/pyang/dtnma_amm.py | 8 +- src/ace/type_constraint.py | 4 +- ...-minimal.yang => example-adm-minimal.yang} | 14 +- tests/test_adm_set.py | 40 +- tests/test_adm_yang.py | 95 +++-- tests/test_ari_cbor.py | 104 ++--- tests/test_ari_roundtrip.py | 20 +- tests/test_ari_text.py | 370 +++++++++--------- tests/test_lookup.py | 16 +- 18 files changed, 520 insertions(+), 397 deletions(-) rename tests/{test-adm-minimal.yang => example-adm-minimal.yang} (64%) diff --git a/src/ace/adm_set.py b/src/ace/adm_set.py index 71496ea..ac913e0 100644 --- a/src/ace/adm_set.py +++ b/src/ace/adm_set.py @@ -48,7 +48,7 @@ def get_modules_and_revisions(self, _ctx): result = [] for adm_mod in found.all(): rev = adm_mod.revisions[0].name if adm_mod.revisions else None - result.append((adm_mod.name, rev, ('yang', adm_mod.source_id))) + result.append((adm_mod.module_name, rev, ('yang', adm_mod.source_id))) for file_entry in self._file_entries: name, ext = os.path.splitext(file_entry.name) diff --git a/src/ace/adm_yang.py b/src/ace/adm_yang.py index 77a4a52..c1b77fd 100644 --- a/src/ace/adm_yang.py +++ b/src/ace/adm_yang.py @@ -128,19 +128,19 @@ class AriTextDecoder: def __init__(self): self._ari_dec = ari_text.Decoder() - self._adm_id = None + self._ns_id = None - def set_adm(self, adm_id: Union[str, int]): + def set_namespace(self, org_id: Union[str, int], model_id: Union[str, int]): ''' Set the ID of the current ADM for resolving relative ARIs. ''' - self._adm_id = adm_id + self._ns_id = (org_id, model_id) def decode(self, text:str) -> ARI: ''' Decode ARI text and resolve any relative reference. ''' ari = self._ari_dec.decode(io.StringIO(text)) - if self._adm_id is not None: - ari = ari.map(RelativeResolver(self._adm_id)) + if self._ns_id is not None: + ari = ari.map(RelativeResolver(*self._ns_id)) return ari @@ -394,7 +394,7 @@ def _check_ari(self, ari:ARI): ''' Verify ARI references only imported modules. ''' if isinstance(ari, ReferenceARI): imports = [mod[0] for mod in self._module.i_prefixes.values()] - if ari.ident.ns_id is not None and ari.ident.ns_id not in imports: + if ari.ident.module_name is not None and ari.ident.module_name not in imports: raise ValueError(f'ARI references module {ari.ident.ns_id} that is not imported') def _get_ari(self, text:str) -> ARI: @@ -617,15 +617,28 @@ def decode(self, buf: TextIO) -> AdmModule: adm = AdmModule() adm.source = src - adm.name = module.arg + adm.module_name = module.arg # Normalize the intrinsic ADM name - adm.norm_name = normalize_ident(adm.name) + adm.norm_name = normalize_ident(adm.module_name) self._adm = adm - self._ari_dec.set_adm(self._adm.norm_name) + + ns_stmt = module.search_one('namespace') + if ns_stmt is None: + raise RuntimeError('ADM module is missing "namespace" statement') + ns_ari = self._ari_dec.decode(ns_stmt.arg) + self._ari_dec.set_namespace(ns_ari.ident.org_id, ns_ari.ident.model_id) + adm.ns_org_name = ns_ari.ident.org_id.casefold() + adm.ns_model_name = ns_ari.ident.model_id.casefold() + + org_stmt = module.search_one('organization') + if org_stmt: + enum_stmt = org_stmt.search_one((AMM_MOD, 'enum')) + if enum_stmt: + adm.ns_org_enum = int(enum_stmt.arg) enum_stmt = module.search_one((AMM_MOD, 'enum')) if enum_stmt: - adm.enum = int(enum_stmt.arg) + adm.ns_model_enum = int(enum_stmt.arg) for sub_stmt in module.search('import'): prefix_stmt = sub_stmt.search_one('prefix') @@ -664,7 +677,7 @@ def decode(self, buf: TextIO) -> AdmModule: self._get_section(adm.oper, Oper, module) self._get_section(adm.var, Var, module) except Exception as err: - raise RuntimeError(f'Failure processing object definitions from ADM "{adm.name}": {err}') from err + raise RuntimeError(f'Failure processing object definitions from ADM "{adm.module_name}": {err}') from err self._module = None return adm @@ -697,15 +710,17 @@ def encode(self, adm: AdmModule, buf: TextIO) -> None: :param adm: The ORM root object. :param buf: The buffer to write into. ''' - module = pyang.statements.new_statement(None, None, None, 'module', adm.name) + module = pyang.statements.new_statement(None, None, None, 'module', adm.module_name) self._module = module self._denorm_prefixes = {} self._add_substmt(module, 'yang-version', '1.1') - self._add_substmt(module, 'namespace', f'ari://{adm.name}/') + self._add_substmt(module, 'namespace', f'ari://{adm.ns_org_name}/{adm.ns_model_name}/') for item in adm.metadata_list.items: - self._add_substmt(module, item.name, item.arg) + item_stmt = self._add_substmt(module, item.name, item.arg) + if item.name == 'organization' and adm.ns_org_enum is not None: + self._add_substmt(item_stmt, (AMM_MOD, 'enum'), str(adm.ns_org_enum)) for imp in adm.imports: imp_stmt = self._add_substmt(module, 'import', imp.name) @@ -719,8 +734,9 @@ def encode(self, adm: AdmModule, buf: TextIO) -> None: modname = modtup[0] self._denorm_prefixes[modname] = prefix - # prefixed keyword after v_init_module - self._add_substmt(module, (AMM_MOD, 'enum'), str(adm.enum)) + if adm.ns_model_enum is not None: + # prefixed keyword after v_init_module + self._add_substmt(module, (AMM_MOD, 'enum'), str(adm.ns_model_enum)) for rev in adm.revisions: sub_stmt = self._add_substmt(module, 'revision', rev.name) diff --git a/src/ace/ari.py b/src/ace/ari.py index 7e21987..4fb2921 100644 --- a/src/ace/ari.py +++ b/src/ace/ari.py @@ -26,7 +26,7 @@ import datetime from dataclasses import dataclass import enum -from typing import Callable, Dict, List, Optional, Union +from typing import Callable, Dict, List, Optional, Tuple, Union import cbor2 import numpy @@ -280,25 +280,44 @@ class Identity: ''' The identity of an object reference as a unique identifer-set. ''' - ns_id:Union[str, int, None] = None - ''' The None value indicates a module-relative path. ''' - ns_rev:Optional[str] = None - ''' For the text-form ARI a specific module revision date. ''' + org_id:Union[str, int, None] = None + ''' The None value indicates an org-relative path. ''' + model_id:Union[str, int, None] = None + ''' The None value indicates an model-relative path. ''' + model_rev:Optional[str] = None + ''' For the text-form ARI a specific ADM revision date. ''' type_id:Optional[StructType] = None ''' ADM type of the referenced object ''' obj_id:Union[str, int, None] = None ''' Name with the type removed ''' - def __str__(self): - ''' Pretty format the identity. + @property + def ns_id(self) -> Tuple: + ''' Get a tuple representing the namespace. ''' + return (self.org_id, self.model_id) + + @property + def module_name(self) -> Optional[str]: + ''' Get the ADM module name associated with this namespace. ''' + if self.org_id is None or self.model_id is None: + return None + return f'{self.org_id}-{self.model_id}' + + def __str__(self) -> str: + ''' Pretty format the identity similar to URI text encoding. ''' text = '' - if self.ns_id is None: - text += '.' + if self.org_id is None: + if self.model_id is None: + text += '.' + else: + text += '..' else: - text += f'/{self.ns_id}' - if self.ns_rev: - text += f'@{self.ns_rev}' + text += f'/{self.org_id}' + if self.model_id is not None: + text += f'/{self.model_id}' + if self.ns_rev: + text += f'@{self.ns_rev}' text += f'/{self.type_id.name}' text += f'/{self.obj_id}' return text diff --git a/src/ace/ari_cbor.py b/src/ace/ari_cbor.py index e2f3244..f89649e 100644 --- a/src/ace/ari_cbor.py +++ b/src/ace/ari_cbor.py @@ -68,20 +68,21 @@ def _item_to_ari(self, item:object): LOGGER.debug('Got ARI item: %s', item) if isinstance(item, list): - if len(item) >= 3: + if len(item) in {4, 5}: # Object reference - type_id = StructType(item[1]) if item[1] is not None else None + type_id = StructType(item[2]) if item[2] is not None else None ident = Identity( - ns_id=item[0], + org_id=item[0], + model_id=item[1], type_id=type_id, - obj_id=item[2], + obj_id=item[3], ) params = None - if len(item) >= 4: + if len(item) == 5: params = [ self._item_to_ari(param_item) - for param_item in item[3] + for param_item in item[4] ] res = ReferenceARI(ident=ident, params=params) @@ -183,7 +184,8 @@ def _ari_to_item(self, obj:ARI) -> object: if isinstance(obj, ReferenceARI): type_id = int(obj.ident.type_id) if obj.ident.type_id is not None else None item = [ - obj.ident.ns_id, + obj.ident.org_id, + obj.ident.model_id, type_id, obj.ident.obj_id, ] diff --git a/src/ace/ari_text/encode.py b/src/ace/ari_text/encode.py index 311685e..7f48e17 100644 --- a/src/ace/ari_text/encode.py +++ b/src/ace/ari_text/encode.py @@ -223,14 +223,20 @@ def _encode_obj(self, buf: TextIO, obj:ARI, prefix:bool=False): elif isinstance(obj, ReferenceARI): if prefix: buf.write('ari:') - if obj.ident.ns_id is None: - buf.write('.') + if obj.ident.org_id is None: + if obj.ident.model_id is None: + buf.write('.') + else: + buf.write('..') else: buf.write('//') - buf.write(str(obj.ident.ns_id)) - if obj.ident.ns_rev is not None: + buf.write(str(obj.ident.org_id)) + if obj.ident.model_id is not None: + buf.write('/') + buf.write(str(obj.ident.model_id)) + if obj.ident.model_rev is not None: buf.write('@') - buf.write(obj.ident.ns_rev) + buf.write(obj.ident.model_rev) buf.write('/') if obj.ident.type_id is not None and obj.ident.obj_id is not None: diff --git a/src/ace/ari_text/parsemod.py b/src/ace/ari_text/parsemod.py index ab794ec..8418b58 100644 --- a/src/ace/ari_text/parsemod.py +++ b/src/ace/ari_text/parsemod.py @@ -209,57 +209,73 @@ def p_params_amlist(p): def p_objpath_only_ns(p): - '''objpath : SLASH SLASH VALSEG - | SLASH SLASH VALSEG SLASH''' - mod = util.MODID(p[3]) + '''objpath : SLASH SLASH VALSEG SLASH VALSEG + | SLASH SLASH VALSEG SLASH VALSEG SLASH''' + org = util.IDSEGMENT(p[3]) + mod = util.MODID(p[5]) if not isinstance(mod, tuple): mod = (mod, None) p[0] = Identity( - ns_id=mod[0], - ns_rev=mod[1], + org_id=org, + model_id=mod[0], + model_rev=mod[1], type_id=None, obj_id=None, ) def p_objpath_with_ns(p): - 'objpath : SLASH SLASH VALSEG SLASH VALSEG SLASH VALSEG' + 'objpath : SLASH SLASH VALSEG SLASH VALSEG SLASH VALSEG SLASH VALSEG' + org = util.IDSEGMENT(p[3]) + mod = util.MODID(p[5]) + if not isinstance(mod, tuple): + mod = (mod, None) + try: - typ = util.get_structtype(p[5]) + typ = util.get_structtype(p[7]) except Exception as err: LOGGER.error('Object type invalid: %s', err) raise RuntimeError(err) from err - # Reference are only allowed with AMM types if typ >= 0 or typ == StructType.OBJECT: raise RuntimeError("Invalid AMM type") - mod = util.MODID(p[3]) - if not isinstance(mod, tuple): - mod = (mod, None) + obj = util.IDSEGMENT(p[9]) p[0] = Identity( - ns_id=mod[0], - ns_rev=mod[1], + org_id=org, + model_id=mod[0], + model_rev=mod[1], type_id=typ, - obj_id=util.IDSEGMENT(p[7]), + obj_id=obj, ) def p_objpath_relative(p): - 'objpath : DOT SLASH VALSEG SLASH VALSEG' + '''objpath : DOT SLASH VALSEG SLASH VALSEG + | DOT DOT SLASH VALSEG SLASH VALSEG SLASH VALSEG''' + got = len(p) + + if got > 6: + mod = util.MODID(p[got - 5]) + if not isinstance(mod, tuple): + mod = (mod, None) + else: + mod = (None, None) + try: - typ = util.get_structtype(p[3]) + typ = util.get_structtype(p[got - 3]) except Exception as err: LOGGER.error('Object type invalid: %s', err) raise RuntimeError(err) from err - # Reference are only allowed with AMM types if typ >= 0 or typ == StructType.OBJECT: raise RuntimeError("Invalid AMM type") - p[0] = Identity(ns_id=None, type_id=typ, obj_id=util.IDSEGMENT(p[5])) + obj = util.IDSEGMENT(p[got - 1]) + + p[0] = Identity(org_id=None, model_id=mod[0], model_rev=mod[1], type_id=typ, obj_id=obj) def p_acbracket(p): diff --git a/src/ace/lookup.py b/src/ace/lookup.py index 9974302..8985cf4 100644 --- a/src/ace/lookup.py +++ b/src/ace/lookup.py @@ -56,30 +56,43 @@ class RelativeResolver: ''' Resolve module-relative ARIs ''' - def __init__(self, ns_id:Union[int, str]): - self._ns_id = ns_id + def __init__(self, org_id: Union[str, int], model_id: Union[str, int]): + self._org_id = org_id + self._model_id = model_id def __call__(self, ari:ARI) -> ARI: if isinstance(ari, ReferenceARI): - if ari.ident.ns_id is None: + if ari.ident.org_id is None or ari.ident.model_id is None: + out_org_id = ari.ident.org_id if ari.ident.org_id is not None else self._org_id + out_mod_id = ari.ident.model_id if ari.ident.model_id is not None else self._model_id ari.ident = Identity( - ns_id=self._ns_id, + org_id=out_org_id, + model_id=out_mod_id, type_id=ari.ident.type_id, obj_id=ari.ident.obj_id, ) return ari -def find_adm(ns_id, db_sess:Session) -> Optional[AdmModule]: +def find_adm(org_id: Union[str, int], model_id: Union[str, int], db_sess:Session) -> Optional[AdmModule]: ''' Dereference an ADM module. ''' query_adm = db_sess.query(AdmModule) - if isinstance(ns_id, int): - query_adm = query_adm.filter(AdmModule.enum == ns_id) - elif isinstance(ns_id, str): - query_adm = query_adm.filter(AdmModule.norm_name == normalize_ident(ns_id)) + + if isinstance(org_id, int): + query_adm = query_adm.filter(AdmModule.ns_org_enum == org_id) + elif isinstance(org_id, str): + query_adm = query_adm.filter(AdmModule.ns_org_name == normalize_ident(org_id)) + else: + raise TypeError(f'ReferenceARI org_id is not int or str: {org_id}') + + if isinstance(model_id, int): + query_adm = query_adm.filter(AdmModule.ns_model_enum == model_id) + elif isinstance(model_id, str): + query_adm = query_adm.filter(AdmModule.ns_model_name == normalize_ident(model_id)) else: - raise TypeError(f'ReferenceARI ns_id is not int or str: {ns_id}') + raise TypeError(f'ReferenceARI model_id is not int or str: {model_id}') + found_adm = query_adm.one_or_none() return found_adm @@ -89,7 +102,7 @@ def dereference(ref:ReferenceARI, db_sess:Session) -> Optional[AdmObjMixin]: ''' orm_type = ORM_TYPE[ref.ident.type_id] - found_adm = find_adm(ref.ident.ns_id, db_sess) + found_adm = find_adm(ref.ident.org_id, ref.ident.model_id, db_sess) if found_adm is None: return None diff --git a/src/ace/models.py b/src/ace/models.py index 053260d..f16b0ec 100644 --- a/src/ace/models.py +++ b/src/ace/models.py @@ -31,7 +31,7 @@ from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.orderinglist import ordering_list -CURRENT_SCHEMA_VERSION = 17 +CURRENT_SCHEMA_VERSION = 18 ''' Value of :attr:`SchemaVersion.version_num` ''' Base = declarative_base() @@ -146,6 +146,18 @@ class AdmSource(Base): ''' Cached full file content. ''' +class Organization(Base): + ''' A namespace organization. ''' + __tablename__ = 'ns_org' + id = Column(Integer, primary_key=True) + ''' Unique ID of the row ''' + + name = Column(String, index=True) + ''' Normalized name of this organization ''' + enum = Column(Integer, index=True) + ''' Enumeration for this organization ''' + + class AdmModule(Base): ''' The ADM itself with relations to its attributes and objects ''' __tablename__ = "adm_module" @@ -159,12 +171,20 @@ class AdmModule(Base): cascade="all, delete" ) - # Normalized ADM name (for searching) - name = Column(String) - # Normalized ADM name (for searching) + module_name = Column(String) + ''' Original module name ''' norm_name = Column(String, index=True) - # Enumeration for this ADM - enum = Column(Integer, index=True) + ''' Normalized module name (for searching) ''' + + ns_org_name = Column(String, index=True) + ''' Namespace organization name ''' + ns_org_enum = Column(Integer, index=True) + ''' Organization enumeration from the module ''' + + ns_model_name = Column(String, index=True) + ''' Name of this model, in normalized form, within the organization ''' + ns_model_enum = Column(Integer, index=True) + ''' Enumeration for this model within the organization ''' metadata_id = Column(Integer, ForeignKey('metadata_list.id'), nullable=False) metadata_list = relationship( diff --git a/src/ace/nickname.py b/src/ace/nickname.py index 8275e80..536dc4a 100644 --- a/src/ace/nickname.py +++ b/src/ace/nickname.py @@ -71,7 +71,7 @@ def _convert_ref(self, ari:ReferenceARI) -> ReferenceARI: if obj: adm = obj.module else: - adm = find_adm(ari.ident.ns_id, self._db_sess) + adm = find_adm(ari.ident.org_id, ari.ident.model_id, self._db_sess) LOGGER.debug('ARI for %s resolved to ADM %s, obj %s', ari.ident, adm, obj) diff --git a/src/ace/pyang/dtnma_amm.py b/src/ace/pyang/dtnma_amm.py index 665eda6..597245b 100644 --- a/src/ace/pyang/dtnma_amm.py +++ b/src/ace/pyang/dtnma_amm.py @@ -67,11 +67,9 @@ def pyang_plugin_init(): # ADM enumeration only at module level and optional # allowing for non-ADM YANG modules grammar.add_to_stmts_rules( - ['module'], + ['module', 'organization'], [((MODULE_NAME, 'enum'), '?')], ) -# rules = grammar.stmt_map['module'][1] -# rules.insert(rules.index(('prefix', '1')) + 1, ((MODULE_NAME, 'enum'), '?')) # AMM object extensions with preferred canonicalization order grammar.add_to_stmts_rules( @@ -537,7 +535,7 @@ def _stmt_check_namespace(ctx:context.Context, stmt:statements.Statement): ns_ref = None if (not isinstance(ns_ref, ReferenceARI) - or ns_ref.ident.ns_id != stmt.main_module().arg.casefold() + or ns_ref.ident.module_name != stmt.main_module().arg.casefold() or ns_ref.ident.type_id is not None or ns_ref.ident.obj_id is not None): error.err_add(ctx.errors, stmt.pos, 'AMM_MODULE_NS', @@ -564,7 +562,7 @@ def visitor(ari): # only care about references with absolute namespace if not isinstance(ari, ReferenceARI): return - if ari.ident.ns_id is None: + if ari.ident.model_id is None: return mod_prefix = mod_map.get(ari.ident.ns_id) diff --git a/src/ace/type_constraint.py b/src/ace/type_constraint.py index c1e7edd..3fc22f8 100644 --- a/src/ace/type_constraint.py +++ b/src/ace/type_constraint.py @@ -179,7 +179,7 @@ class IdentRefBase(Constraint): ''' Original required base text. ''' base_ari:ReferenceARI ''' The base object reference. ''' - base_ident:Ident = None + base_ident:Optional[Ident] = None ''' ADM object lookup session ''' def applicable(self) -> Set[StructType]: @@ -187,6 +187,8 @@ def applicable(self) -> Set[StructType]: def is_valid(self, obj:ARI) -> bool: if isinstance(obj, ReferenceARI) and obj.ident.type_id == StructType.IDENT: + if self.base_ident is None: + return False db_sess = object_session(self.base_ident) def match_base(ref:ARI): diff --git a/tests/test-adm-minimal.yang b/tests/example-adm-minimal.yang similarity index 64% rename from tests/test-adm-minimal.yang rename to tests/example-adm-minimal.yang index 93e1d48..53d71ea 100644 --- a/tests/test-adm-minimal.yang +++ b/tests/example-adm-minimal.yang @@ -1,16 +1,20 @@ -module test-adm-minimal { - namespace "ari://test-adm-minimal/"; +module example-adm-minimal { + namespace "ari://example/adm-minimal/"; prefix test; import ietf-amm { prefix amm; } + organization "example" { + amm:enum 65535; + } revision 2023-10-31 { description "Initial test"; } - amm:enum "0"; + amm:enum 0; + amm:edd edd1 { amm:type /ARITYPE/int; description @@ -18,10 +22,10 @@ module test-adm-minimal { } amm:ctrl test1 { amm:parameter id { - amm:type "//ietf-amm/TYPEDEF/any"; + amm:type "//ietf/amm/TYPEDEF/any"; } amm:parameter def { - amm:type "//ietf-amm/TYPEDEF/expr"; + amm:type "//ietf/amm/TYPEDEF/expr"; } description "This control resets all Agent ADM statistics reported in the Agent ADM report."; diff --git a/tests/test_adm_set.py b/tests/test_adm_set.py index 4bedb2c..aa666a8 100644 --- a/tests/test_adm_set.py +++ b/tests/test_adm_set.py @@ -73,7 +73,7 @@ def test_load_from_dir(self): # one new ADM os.makedirs(adms_path) - shutil.copy(os.path.join(SELFDIR, 'test-adm-minimal.yang'), adms_path) + shutil.copy(os.path.join(SELFDIR, 'example-adm-minimal.yang'), adms_path) self.assertEqual(1, adms.load_from_dirs([adms_path])) self.assertEqual(1, len(adms)) @@ -84,7 +84,7 @@ def test_load_from_dir(self): self.assertEqual(1, len(adms)) # updated file - with open(os.path.join(adms_path, 'test-adm-minimal.yang'), 'ab') as outfile: + with open(os.path.join(adms_path, 'example-adm-minimal.yang'), 'ab') as outfile: outfile.write(b'\r\n') self.assertEqual(1, adms.load_from_dirs([adms_path])) self.assertEqual(1, len(adms)) @@ -95,51 +95,51 @@ def test_load_default_dirs(self): self.assertEqual(0, adms.load_default_dirs()) self.assertEqual(0, len(adms)) - self.assertNotIn('test-adm-minimal', adms) + self.assertNotIn('example-adm-minimal', adms) with self.assertRaises(KeyError): - adms['test-adm-minimal'] # pylint: disable=pointless-statement + adms['example-adm-minimal'] # pylint: disable=pointless-statement self.assertEqual(frozenset(), adms.names()) adms_path = os.path.join(os.environ['XDG_DATA_HOME'], 'ace', 'adms') os.makedirs(adms_path) - shutil.copy(os.path.join(SELFDIR, 'test-adm-minimal.yang'), adms_path) + shutil.copy(os.path.join(SELFDIR, 'example-adm-minimal.yang'), adms_path) self.assertEqual(1, adms.load_default_dirs()) self.assertEqual(1, len(adms)) - self.assertIn('test-adm-minimal', adms) - self.assertIsInstance(adms['test-adm-minimal'], AdmModule) - self.assertEqual(frozenset(['test-adm-minimal']), adms.names()) + self.assertIn('example-adm-minimal', adms) + self.assertIsInstance(adms['example-adm-minimal'], AdmModule) + self.assertEqual(frozenset(['example-adm-minimal']), adms.names()) for adm in adms: self.assertIsInstance(adm, AdmModule) def test_load_from_file(self): adms = AdmSet() self.assertEqual(0, len(adms)) - self.assertNotIn('test-adm-minimal', adms) + self.assertNotIn('example-adm-minimal', adms) - file_path = os.path.join(SELFDIR, 'test-adm-minimal.yang') + file_path = os.path.join(SELFDIR, 'example-adm-minimal.yang') adm_new = adms.load_from_file(file_path) self.assertIsNotNone(adm_new.id) - self.assertEqual('test-adm-minimal', adm_new.norm_name) + self.assertEqual('example-adm-minimal', adm_new.norm_name) self.assertEqual(1, len(adms)) - self.assertIn('test-adm-minimal', adms) + self.assertIn('example-adm-minimal', adms) # Still only one ADM after loading adm_next = adms.load_from_file(file_path) self.assertIsNotNone(adm_new.id) - self.assertEqual('test-adm-minimal', adm_next.norm_name) + self.assertEqual('example-adm-minimal', adm_next.norm_name) # Identical object due to cache self.assertEqual(adm_new.id, adm_next.id) self.assertEqual(1, len(adms)) - self.assertIn('test-adm-minimal', adms) + self.assertIn('example-adm-minimal', adms) def test_load_from_data(self): adms = AdmSet() self.assertEqual(0, len(adms)) - self.assertNotIn('test-adm-minimal', adms) + self.assertNotIn('example-adm-minimal', adms) - file_path = os.path.join(SELFDIR, 'test-adm-minimal.yang') + file_path = os.path.join(SELFDIR, 'example-adm-minimal.yang') buf = io.StringIO() with open(file_path, 'r') as infile: buf.write(infile.read()) @@ -147,9 +147,9 @@ def test_load_from_data(self): buf.seek(0) adm_new = adms.load_from_data(buf) self.assertIsNotNone(adm_new.id) - self.assertEqual('test-adm-minimal', adm_new.norm_name) + self.assertEqual('example-adm-minimal', adm_new.norm_name) self.assertEqual(1, len(adms)) - self.assertIn('test-adm-minimal', adms) + self.assertIn('example-adm-minimal', adms) buf.seek(0) adm_next = adms.load_from_data(buf, del_dupe=True) @@ -157,11 +157,11 @@ def test_load_from_data(self): # Non-identical due to replacement self.assertNotEqual(adm_new.id, adm_next.id) self.assertEqual(1, len(adms)) - self.assertIn('test-adm-minimal', adms) + self.assertIn('example-adm-minimal', adms) buf.seek(0) adm_next = adms.load_from_data(buf, del_dupe=False) self.assertIsNotNone(adm_new.id) self.assertNotEqual(adm_new.id, adm_next.id) self.assertEqual(2, len(adms)) - self.assertIn('test-adm-minimal', adms) + self.assertIn('example-adm-minimal', adms) diff --git a/tests/test_adm_yang.py b/tests/test_adm_yang.py index cfc0174..c4f80b6 100644 --- a/tests/test_adm_yang.py +++ b/tests/test_adm_yang.py @@ -101,18 +101,23 @@ def _from_text(self, text:str) -> ari.ARI: NOOBJECT_MODULE_HEAD = '''\ module example-mod { yang-version 1.1; - namespace "ari://example-mod/"; + namespace "ari://example/mod/"; prefix empty; import ietf-amm { prefix amm; } + organization + "example" { + amm:enum 65535; + } + revision 2023-10-31 { description "Initial test"; } - amm:enum 65536; + amm:enum 1; ''' NOOBJECT_MODULE_TAIL = '''\ } @@ -132,7 +137,9 @@ class TestAdmYang(BaseYang): ''' Tests of the YANG-based syntax handler separate from ADM logic. ''' EMPTY_MODULE = '''\ -module empty {} +module example-empty { + namespace "ari://example/empty/"; +} ''' def test_decode_empty(self): @@ -142,7 +149,11 @@ def test_decode_empty(self): self._db_sess.add(adm) self._db_sess.commit() - self.assertEqual('empty', adm.name) + self.assertEqual('example-empty', adm.module_name) + self.assertEqual('example', adm.ns_org_name) + self.assertIsNone(adm.ns_org_enum) + self.assertEqual('empty', adm.ns_model_name) + self.assertIsNone(adm.ns_model_enum) def test_decode_noobject(self): buf = self._get_mod_buf('') @@ -152,8 +163,12 @@ def test_decode_noobject(self): self._db_sess.commit() self.assertIsNone(adm.source.abs_file_path) - self.assertEqual('example-mod', adm.name) - self.assertEqual('example-mod', adm.norm_name) + self.assertEqual('example-mod', adm.module_name) + self.assertEqual('example', adm.ns_org_name) + self.assertEqual(65535, adm.ns_org_enum) + self.assertEqual('mod', adm.ns_model_name) + self.assertEqual(1, adm.ns_model_enum) + self.assertEqual(1, len(adm.imports)) self.assertEqual(1, len(adm.revisions)) self.assertEqual(0, len(adm.typedef)) @@ -174,10 +189,10 @@ def test_decode_minimal(self): amm:ctrl test1 { amm:enum 5; amm:parameter id { - amm:type "//ietf-amm/TYPEDEF/any"; + amm:type "//ietf/amm/TYPEDEF/any"; } amm:parameter def { - amm:type "//ietf-amm/TYPEDEF/expr"; + amm:type "//ietf/amm/TYPEDEF/expr"; } } ''') @@ -187,8 +202,11 @@ def test_decode_minimal(self): self._db_sess.commit() self.assertIsNone(adm.source.abs_file_path) - self.assertEqual('example-mod', adm.name) - self.assertEqual('example-mod', adm.norm_name) + self.assertEqual('example-mod', adm.module_name) + self.assertEqual('example', adm.ns_org_name) + self.assertEqual(65535, adm.ns_org_enum) + self.assertEqual('mod', adm.ns_model_name) + self.assertEqual(1, adm.ns_model_enum) self.assertEqual(1, len(adm.imports)) self.assertEqual(1, len(adm.revisions)) @@ -199,7 +217,7 @@ def test_decode_minimal(self): self.assertEqual(2, len(obj.parameters.items)) self.assertEqual("id", obj.parameters.items[0].name) self.assertEqual( - self._from_text('//ietf-amm/typedef/any'), + self._from_text('//ietf/amm/typedef/any'), obj.parameters.items[0].typeobj.type_ari ) @@ -222,10 +240,10 @@ def test_decode_groupings(self): } grouping paramgrp { amm:parameter id { - amm:type "//ietf-amm/TYPEDEF/any"; + amm:type "//ietf/amm/TYPEDEF/any"; } amm:parameter def { - amm:type "//ietf-amm/TYPEDEF/expr"; + amm:type "//ietf/amm/TYPEDEF/expr"; amm:default "ari:/AC/()"; } } @@ -240,8 +258,11 @@ def test_decode_groupings(self): self._db_sess.commit() self.assertIsNone(adm.source.abs_file_path) - self.assertEqual('example-mod', adm.name) - self.assertEqual('example-mod', adm.norm_name) + self.assertEqual('example-mod', adm.module_name) + self.assertEqual('example', adm.ns_org_name) + self.assertEqual(65535, adm.ns_org_enum) + self.assertEqual('mod', adm.ns_model_name) + self.assertEqual(1, adm.ns_model_enum) self.assertEqual(1, len(adm.imports)) self.assertEqual(1, len(adm.revisions)) @@ -254,7 +275,7 @@ def test_decode_groupings(self): param = obj.parameters.items[0] self.assertEqual("id", param.name) self.assertEqual( - self._from_text('//ietf-amm/typedef/any'), + self._from_text('//ietf/amm/typedef/any'), param.typeobj.type_ari ) self.assertIsNone(param.default_value) @@ -262,7 +283,7 @@ def test_decode_groupings(self): param = obj.parameters.items[1] self.assertEqual("def", param.name) self.assertEqual( - self._from_text('//ietf-amm/typedef/expr'), + self._from_text('//ietf/amm/typedef/expr'), param.typeobj.type_ari ) self.assertEqual("ari:/AC/()", param.default_value) @@ -477,7 +498,7 @@ def test_decode_groupings(self): amm:type "/ARITYPE/INT"; } amm:parameter two { - amm:type "//ietf-amm/TYPEDEF/expr"; + amm:type "//ietf/amm/TYPEDEF/expr"; } } ''', @@ -505,11 +526,11 @@ def test_decode_groupings(self): } amm:operand vals { amm:seq { - amm:type "//ietf-amm/TYPEDEF/numeric"; + amm:type "//ietf/amm/TYPEDEF/numeric"; } } amm:result total { - amm:type "//ietf-amm/TYPEDEF/numeric"; + amm:type "//ietf/amm/TYPEDEF/numeric"; } } ''', @@ -679,14 +700,14 @@ class TestAdmContents(BaseYang): ('''\ amm:typedef typeobj { amm:type "/ARITYPE/IDENT" { - amm:base "//ietf-amm/IDENT/somename"; + amm:base "//ietf/amm/IDENT/somename"; } } ''', True), ('''\ amm:typedef typeobj { amm:type "/ARITYPE/TEXTSTR" { - amm:base "//ietf-amm/IDENT/somename"; + amm:base "//ietf/amm/IDENT/somename"; } } ''', False), @@ -744,8 +765,11 @@ def test_ident_base_constraint(self): self._db_sess.commit() self.assertIsNone(adm.source.abs_file_path) - self.assertEqual('example-mod', adm.name) - self.assertEqual('example-mod', adm.norm_name) + self.assertEqual('example-mod', adm.module_name) + self.assertEqual('example', adm.ns_org_name) + self.assertEqual(65535, adm.ns_org_enum) + self.assertEqual('mod', adm.ns_model_name) + self.assertEqual(1, adm.ns_model_enum) self.assertEqual(1, len(adm.imports)) self.assertEqual(1, len(adm.revisions)) @@ -756,19 +780,19 @@ def test_ident_base_constraint(self): self.assertEqual('type-any', type_any.norm_name) typeobj_any = lookup.TypeResolver().resolve(type_any.typeobj, adm) self.assertIsNone(typeobj_any.get(self._from_text('hi'))) - self.assertIsNotNone(typeobj_any.get(self._from_text('//example-mod/IDENT/ident-z'))) - self.assertIsNotNone(typeobj_any.get(self._from_text('//example-mod/IDENT/ident-a'))) - self.assertIsNotNone(typeobj_any.get(self._from_text('//example-mod/IDENT/ident-b'))) - self.assertIsNotNone(typeobj_any.get(self._from_text('//example-mod/IDENT/ident-c'))) + self.assertIsNotNone(typeobj_any.get(self._from_text('//example/mod/IDENT/ident-z'))) + self.assertIsNotNone(typeobj_any.get(self._from_text('//example/mod/IDENT/ident-a'))) + self.assertIsNotNone(typeobj_any.get(self._from_text('//example/mod/IDENT/ident-b'))) + self.assertIsNotNone(typeobj_any.get(self._from_text('//example/mod/IDENT/ident-c'))) type_a = adm.typedef[1] self.assertEqual('type-a', type_a.norm_name) typeobj_a = lookup.TypeResolver().resolve(type_a.typeobj, adm) self.assertIsNone(typeobj_a.get(self._from_text('hi'))) - self.assertIsNone(typeobj_a.get(self._from_text('//example-mod/IDENT/ident-z'))) - self.assertIsNotNone(typeobj_a.get(self._from_text('//example-mod/IDENT/ident-a'))) - self.assertIsNotNone(typeobj_a.get(self._from_text('//example-mod/IDENT/ident-b'))) - self.assertIsNone(typeobj_a.get(self._from_text('//example-mod/IDENT/ident-c'))) + self.assertIsNone(typeobj_a.get(self._from_text('//example/mod/IDENT/ident-z'))) + self.assertIsNotNone(typeobj_a.get(self._from_text('//example/mod/IDENT/ident-a'))) + self.assertIsNotNone(typeobj_a.get(self._from_text('//example/mod/IDENT/ident-b'))) + self.assertIsNone(typeobj_a.get(self._from_text('//example/mod/IDENT/ident-c'))) def test_ident_params(self): buf = self._get_mod_buf(''' @@ -789,8 +813,11 @@ def test_ident_params(self): self._db_sess.commit() self.assertIsNone(adm.source.abs_file_path) - self.assertEqual('example-mod', adm.name) - self.assertEqual('example-mod', adm.norm_name) + self.assertEqual('example-mod', adm.module_name) + self.assertEqual('example', adm.ns_org_name) + self.assertEqual(65535, adm.ns_org_enum) + self.assertEqual('mod', adm.ns_model_name) + self.assertEqual(1, adm.ns_model_enum) self.assertEqual(1, len(adm.imports)) self.assertEqual(1, len(adm.revisions)) diff --git a/tests/test_ari_cbor.py b/tests/test_ari_cbor.py index dfe5781..c84d267 100644 --- a/tests/test_ari_cbor.py +++ b/tests/test_ari_cbor.py @@ -89,14 +89,14 @@ def test_literal_cbor_loopback(self): ) REFERENCE_DATAS = [ - # from `ari://0/` - cbor2.dumps([0, None, None]), - # from `ari://65536/` - cbor2.dumps([65536, None, None]), - # from `ari://0/CTRL/0` - cbor2.dumps([0, StructType.CTRL.value, 0]), - # from 'ari:/bp-agent/CTRL/reset_all_counts()', - cbor2.dumps([0, StructType.CTRL.value, 10]), + # from `ari://65535/1/` + cbor2.dumps([65535, 1, None, None]), + # from `ari://65535/hi/` + cbor2.dumps([65535, "hi", None, None]), + # from `ari://65535/1/CTRL/0` + cbor2.dumps([65535, 1, StructType.CTRL.value, 0]), + # from 'ari:/ietf/bp-agent/CTRL/reset_all_counts()', + cbor2.dumps([1, "bp-agent", StructType.CTRL.value, 10]), ] def test_reference_cbor_loopback(self): @@ -143,24 +143,23 @@ def test_invalid_enc_failure(self): def test_ari_cbor_encode_objref_path_text(self): TEST_CASE = [ - ("example-adm-a@2024-06-25", None, None, - b"8378186578616D706C652D61646D2D6140323032342D30362D3235F6F6"), - ("example-adm-a", None, None, b"836D6578616D706C652D61646D2D61F6F6"), - ("!example-odm-b", None, None, b"836E216578616D706C652D6F646D2D62F6F6"), - ("adm", None, None, b"836361646DF6F6"), - (None, StructType.CONST, "hi", b"83F621626869"), - ("adm", StructType.CONST, "hi", b"836361646D21626869"), - ("test", StructType.CONST, "that", b"836474657374216474686174"), - ("test@1234", StructType.CONST, "that", b"8369746573744031323334216474686174"), - ("!test", StructType.CONST, "that", b"83652174657374216474686174"), + ("example", "adm-a@2024-06-25", None, None, b"84676578616D706C657061646D2D6140323032342D30362D3235F6F6"), + ("example", "adm-a", None, None, b"84676578616D706C656561646D2D61F6F6"), + ("example", "!odm-b", None, None, b"84676578616D706C6566216F646D2D62F6F6"), + (65535, "adm", None, None, b"8419FFFF6361646DF6F6"), + (None, None, StructType.CONST, "hi", b"84F6F621626869"), + (65535, "adm", StructType.CONST, "hi", b"8419FFFF6361646D21626869"), + (65535, "test", StructType.CONST, "that", b"8419FFFF6474657374216474686174"), + (65535, "test@1234", StructType.CONST, "that", b"8419FFFF69746573744031323334216474686174"), + (65535, "!test", StructType.CONST, "that", b"8419FFFF652174657374216474686174"), ] enc = ari_cbor.Encoder() for row in TEST_CASE: - ns_id, type_id, obj, expect = row + org_id, model_id, type_id, obj_id, expect = row with self.subTest(expect): ari = ReferenceARI( - ident=Identity(ns_id, None, type_id, obj), + ident=Identity(org_id=org_id, model_id=model_id, type_id=type_id, obj_id=obj_id), params=None ) loop = io.BytesIO() @@ -175,67 +174,68 @@ def test_ari_cbor_encode_objref_path_text(self): def test_ari_cbor_encode_objref_path_int(self): TEST_CASE = [ - (18, None, None, b"8312F6F6"), - (65536, None, None, b"831A00010000F6F6"), - (-20, None, None, b"8333F6F6"), - (None, StructType.IDENT, 34, b"83F6201822"), - (18, StructType.IDENT, 34, b"8312201822"), + (65535, 18, None, None, b"8419FFFF12F6F6"), + (65535, -20, None, None, b"8419FFFF33F6F6"), + (None, None, StructType.IDENT, 34, b"84F6F6201822"), + (65535, 18, StructType.IDENT, 34, b"8419FFFF12201822"), ] enc = ari_cbor.Encoder() for row in TEST_CASE: - ns_id, type_id, obj_id, expect = row + org_id, model_id, type_id, obj_id, expect = row ari = ReferenceARI( - ident=Identity(ns_id, None, type_id, obj_id), + ident=Identity(org_id=org_id, model_id=model_id, type_id=type_id, obj_id=obj_id), params=None ) - loop = io.BytesIO() - enc.encode(ari, loop) - LOGGER.info('Got data: %s', to_diag(loop.getvalue())) + buf = io.BytesIO() + enc.encode(ari, buf) + LOGGER.info('Got data: %s', to_diag(buf.getvalue())) self.assertEqual( - base64.b16encode(loop.getvalue()), + base64.b16encode(buf.getvalue()), expect) def test_ari_cbor_decode_objref_path_text(self): TEST_CASE = [ - ("836361646D21626869", "adm", StructType.CONST, "hi"), - ("836474657374216474686174", "test", StructType.CONST, "that"), - ("8369746573744031323334216474686174", "test@1234", StructType.CONST, "that"), - ("83652174657374216474686174", "!test", StructType.CONST, "that"), - ("846474657374226474686174811822", "test", StructType.CTRL, "that"), + ("84676578616D706C656361646D21626869", "example", "adm", StructType.CONST, "hi"), + ("84676578616D706C656474657374216474686174", "example", "test", StructType.CONST, "that"), + ("84676578616D706C6569746573744031323334216474686174", "example", "test@1234", StructType.CONST, "that"), + ("84676578616D706C65652174657374216474686174", "example", "!test", StructType.CONST, "that"), + ("85676578616D706C656474657374226474686174811822", "example", "test", StructType.CTRL, "that"), ] dec = ari_cbor.Decoder() for row in TEST_CASE: - data, expect_ns_id, expect_type_id, expect_obj_id = row + data, expect_org_id, expect_model_id, expect_type_id, expect_obj_id = row data = base64.b16decode(data) LOGGER.info('Testing data: %s', to_diag(data)) ari = dec.decode(io.BytesIO(data)) - self.assertEqual(ari.ident.ns_id, expect_ns_id) + self.assertEqual(ari.ident.org_id, expect_org_id) + self.assertEqual(ari.ident.model_id, expect_model_id) self.assertEqual(ari.ident.type_id, expect_type_id) self.assertEqual(ari.ident.obj_id, expect_obj_id) def test_ari_cbor_decode_objref_path_int(self): TEST_CASE = [ - ("8312201822", 18, StructType.IDENT, 34), - ("8402220481626869", 2, StructType.CTRL, 4), + ("8419FFFF12201822", 65535, 18, StructType.IDENT, 34), + ("8519FFFF02220481626869", 65535, 2, StructType.CTRL, 4), ] dec = ari_cbor.Decoder() for row in TEST_CASE: - data, expect_ns_id, expect_type_id, expect_obj_id = row + data, expect_org_id, expect_model_id, expect_type_id, expect_obj_id = row data = base64.b16decode(data) LOGGER.info('Testing data: %s', to_diag(data)) ari = dec.decode(io.BytesIO(data)) - self.assertEqual(ari.ident.ns_id, expect_ns_id) + self.assertEqual(ari.ident.org_id, expect_org_id) + self.assertEqual(ari.ident.model_id, expect_model_id) self.assertEqual(ari.ident.type_id, expect_type_id) self.assertEqual(ari.ident.obj_id, expect_obj_id) def test_ari_cbor_decode_rptset(self): TEST_CASE = [ - ("8215831904D21903E8850083647465737422626869F603426869", 1234, 1000, 0, + ("8215831904D21903E885008419FFFF647465737422626869F603426869", 1234, 1000, 0, 1), - ("8215831904D282211904D2850083647465737422626869F603426869", 1234, 12, 340000000, 1) + ("8215831904D282211904D285008419FFFF647465737422626869F603426869", 1234, 12, 340000000, 1) ] dec = ari_cbor.Decoder() @@ -496,7 +496,7 @@ def test_ari_cbor_loopback(self): ("82061864"), ("82071864"), ("8212A303F50A626869626F6804"), - ("8464746573742A6474686174811822"), + ("85676578616D706C6564746573742A6474686174811822"), ("F5"), ("F4"), ("1904D2"), @@ -533,15 +533,15 @@ def test_ari_cbor_loopback(self): # ("82138100"), ("82138101"), ("821481F6"), - ("8214821904D283647465737422626869"), - ("8214834268698364746573742262686983647465737422626568"), + ("8214821904D28419FFFF647465737422626869"), + ("8214834268698419FFFF6474657374226268698419FFFF647465737422626568"), # ("8215831904D21903E8850083647465737422626869F603426869"), # ("8215831904D21A2B450625850083647465737422626869F603426869"), - ("836474657374216474686174"), - ("8369746573744031323334216474686174"), - ("83652174657374216474686174"), - ("846474657374226474686174811822"), - ("8402220481626869"), + ("8419FFFF6474657374216474686174"), + ("8419FFFF69746573744031323334216474686174"), + ("8419FFFF652174657374216474686174"), + ("8519FFFF6474657374226474686174811822"), + ("8519FFFF02220481626869"), ("820F410A"), ("820F4BA164746573748203F94480"), ] diff --git a/tests/test_ari_roundtrip.py b/tests/test_ari_roundtrip.py index ac38983..b2c4933 100644 --- a/tests/test_ari_roundtrip.py +++ b/tests/test_ari_roundtrip.py @@ -60,18 +60,18 @@ class TestAriRoundtrip(unittest.TestCase): 'ari:/AM/(1=1,2=3)', 'ari:/TBL/c=3;', 'ari:/TBL/c=3;(1,2,3)(a,b,c)', - 'ari:/EXECSET/n=1234;(//adm/CTRL/name)', - 'ari:/RPTSET/n=null;r=20240102T030405Z;(t=PT;s=//adm/CTRL/name;(null))', + 'ari:/EXECSET/n=1234;(//example/adm/CTRL/name)', + 'ari:/RPTSET/n=null;r=20240102T030405Z;(t=PT;s=//example/adm/CTRL/name;(null))', # Reference - 'ari://65536/VAR/0', - 'ari://4294967296/VAR/2', - 'ari://namespace/VAR/hello', - 'ari://namespace/VAR/hello()', - 'ari://namespace/VAR/hello(/INT/10)', - 'ari://bp-agent/CTRL/reset_all_counts()', - 'ari://amp-agent/CTRL/gen_rpts(/AC/(//DTN.bpsec/CONST/source_report(%22ipn%3A1.1%22)),/AC/())', + 'ari://65536/65536/VAR/0', + 'ari://4294967296/4294967296/VAR/2', + 'ari://org/model/VAR/hello', + 'ari://org/model/VAR/hello()', + 'ari://org/model/VAR/hello(/INT/10)', + 'ari://ietf/bp-agent/CTRL/reset_all_counts()', + 'ari://ietf/amp-agent/CTRL/gen_rpts(/AC/(//ietf/bpsec/CONST/source_report(%22ipn%3A1.1%22)),/AC/())', # Per spec: - 'ari://AMP-AGENT/CTRL/ADD_SBR(//APL_SC/SBR/HEAT_ON,/VAST/0,/AC/(//APL_SC/EDD/payload_temperature,//APL_SC/CONST/payload_heat_on_temp,//AMP.AGENT/OPER/LESSTHAN),/VAST/1000,/VAST/1000,/AC/(//APL_SC/CTRL/payload_heater(/INT/1)),%22heater%20on%22)', + 'ari://ietf/AMP-AGENT/CTRL/ADD_SBR(//APL/SC/SBR/HEAT_ON,/VAST/0,/AC/(//APL/SC/EDD/payload_temperature,//APL/SC/CONST/payload_heat_on_temp,//ietf/AMP-AGENT/OPER/LESSTHAN),/VAST/1000,/VAST/1000,/AC/(//APL/SC/CTRL/payload_heater(/INT/1)),%22heater%20on%22)', ] def test_text_cbor_roundtrip(self): diff --git a/tests/test_ari_text.py b/tests/test_ari_text.py index b9319dd..bfef109 100644 --- a/tests/test_ari_text.py +++ b/tests/test_ari_text.py @@ -157,31 +157,31 @@ def assertEqualWithNan(self, aval, bval): # pylint: disable=invalid-name ]) ), ( - 'ari:/EXECSET/n=null;(//adm/CTRL/name)', + 'ari:/EXECSET/n=null;(//example/adm/CTRL/name)', ExecutionSet(nonce=None, targets=[ - ReferenceARI(Identity(ns_id='adm', type_id=StructType.CTRL, obj_id='name')) + ReferenceARI(Identity(org_id='example', model_id='adm', type_id=StructType.CTRL, obj_id='name')) ]) ), ( - 'ari:/EXECSET/n=1234;(//adm/CTRL/name)', + 'ari:/EXECSET/n=1234;(//example/adm/CTRL/name)', ExecutionSet(nonce=1234, targets=[ - ReferenceARI(Identity(ns_id='adm', type_id=StructType.CTRL, obj_id='name')) + ReferenceARI(Identity(org_id='example', model_id='adm', type_id=StructType.CTRL, obj_id='name')) ]) ), ( - 'ari:/EXECSET/n=h%276869%27;(//adm/CTRL/name)', + 'ari:/EXECSET/n=h%276869%27;(//example/adm/CTRL/name)', ExecutionSet(nonce=b'hi', targets=[ - ReferenceARI(Identity(ns_id='adm', type_id=StructType.CTRL, obj_id='name')) + ReferenceARI(Identity(org_id='example', model_id='adm', type_id=StructType.CTRL, obj_id='name')) ]) ), ( - 'ari:/RPTSET/n=null;r=20240102T030405Z;(t=PT;s=//adm/CTRL/name;(null))', + 'ari:/RPTSET/n=null;r=20240102T030405Z;(t=PT;s=//example/adm/CTRL/name;(null))', ReportSet( nonce=None, ref_time=datetime.datetime(2024, 1, 2, 3, 4, 5), reports=[ Report( - source=ReferenceARI(Identity(ns_id='adm', type_id=StructType.CTRL, obj_id='name')), + source=ReferenceARI(Identity(org_id='example', model_id='adm', type_id=StructType.CTRL, obj_id='name')), rel_time=datetime.timedelta(seconds=0), items=[ LiteralARI(None) @@ -191,13 +191,13 @@ def assertEqualWithNan(self, aval, bval): # pylint: disable=invalid-name ) ), ( - 'ari:/RPTSET/n=1234;r=20240102T030405Z;(t=PT;s=//adm/CTRL/other;(null))', + 'ari:/RPTSET/n=1234;r=20240102T030405Z;(t=PT;s=//example/adm/CTRL/other;(null))', ReportSet( nonce=1234, ref_time=datetime.datetime(2024, 1, 2, 3, 4, 5), reports=[ Report( - source=ReferenceARI(Identity(ns_id='adm', type_id=StructType.CTRL, obj_id='other')), + source=ReferenceARI(Identity(org_id='example', model_id='adm', type_id=StructType.CTRL, obj_id='other')), rel_time=datetime.timedelta(seconds=0), items=[ LiteralARI(None) @@ -269,22 +269,21 @@ def test_literal_text_options(self): self.assertEqual(ari_dn, ari_up) REFERENCE_TEXTS = [ - 'ari://0/', - 'ari://65536/', - 'ari://namespace/', - 'ari://!namespace/', - 'ari://namespace/VAR/hello', - 'ari://!namespace/VAR/hello', - 'ari://namespace/VAR/hello()', - 'ari://namespace/VAR/hello(/INT/10)', - 'ari://namespace/VAR/hello(//other/CONST/hi)', - 'ari://namespace@2020-01-01/VAR/hello', - 'ari://0/CTRL/0', + 'ari://65535/0/', + 'ari://example/namespace/', + 'ari://example/!namespace/', + 'ari://example/namespace/VAR/hello', + 'ari://example/!namespace/VAR/hello', + 'ari://example/namespace/VAR/hello()', + 'ari://example/namespace/VAR/hello(/INT/10)', + 'ari://example/namespace/VAR/hello(//example/other/CONST/hi)', + 'ari://example/namespace@2020-01-01/VAR/hello', + 'ari://65535/0/CTRL/0', 'ari:./VAR/hello', - 'ari://bp-agent/CTRL/reset_all_counts()', - 'ari://amp-agent/CTRL/gen_rpts(/AC/(//bpsec/CONST/source_report(%22ipn%3A1.1%22)),/AC/())', + 'ari://ietf/bp-agent/CTRL/reset_all_counts()', + 'ari://ietf/amp-agent/CTRL/gen_rpts(/AC/(//ietf/bpsec/CONST/source_report(%22ipn%3A1.1%22)),/AC/())', # Per spec: - 'ari://amp-agent/CTRL/ADD_SBR(//APL_SC/SBR/HEAT_ON,/VAST/0,/AC/(//APL_SC/EDD/payload_temperature,//APL_SC/CONST/payload_heat_on_temp,//amp-agent/OPER/LESSTHAN),/VAST/1000,/VAST/1000,/AC/(//APL_SC/CTRL/payload_heater(/INT/1)),%22heater%20on%22)', + 'ari://ietf/amp-agent/CTRL/ADD_SBR(//APL/SC/SBR/HEAT_ON,/VAST/0,/AC/(//APL/SC/EDD/payload_temperature,//APL/SC/CONST/payload_heat_on_temp,//ietf/amp-agent/OPER/LESSTHAN),/VAST/1000,/VAST/1000,/AC/(//APL/SC/CTRL/payload_heater(/INT/1)),%22heater%20on%22)', ] def test_reference_text_loopback(self): @@ -314,7 +313,7 @@ def test_reference_text_loopback(self): ('/AM/()', '/AM/' '/AM/3'), ('/TBL/c=1;', '/TBL/' '/TBL/c=1;(1,2)'), ('/LABEL/hi', '/LABEL/3', '/LABEL/%22hi%22'), - ('ari://ns/EDD/hello', 'ari://ns/EDD/hello(('), + ('ari://example/ns/EDD/hello', 'ari://example/ns/EDD/hello(('), ('ari:./EDD/hello', 'ari://./EDD/hello', 'ari:/./EDD/hello'), ] ''' Valid ARI followed by invalid variations ''' @@ -336,12 +335,13 @@ def test_invalid_text_failure(self): LOGGER.info('Instead got ARI %s', ari) def test_complex_decode(self): - text = 'ari://amp-agent/CTRL/gen_rpts(/AC/(//bpsec/CONST/source_report(%22ipn%3A1.1%22)),/AC/())' + text = 'ari://ietf/amp-agent/CTRL/gen_rpts(/AC/(//ietf/bpsec/CONST/source_report(%22ipn%3A1.1%22)),/AC/())' dec = ari_text.Decoder() ari = dec.decode(io.StringIO(text)) LOGGER.info('Got ARI %s', ari) self.assertIsInstance(ari, ARI) - self.assertEqual(ari.ident.ns_id, 'amp-agent') + self.assertEqual(ari.ident.org_id, 'ietf') + self.assertEqual(ari.ident.model_id, 'amp-agent') self.assertEqual(ari.ident.type_id, StructType.CTRL) self.assertEqual(ari.ident.obj_id, 'gen_rpts') self.assertIsInstance(ari.params[0], LiteralARI) @@ -360,18 +360,17 @@ def test_ari_text_encode_lit_prim_int(self): (-1234, 16, "ari:-0x4D2"), ] - #encoder test + # encoder test for row in TEST_CASE: value, base, expect = row with self.subTest(value): - enc = ari_text.Encoder(int_base = base) + enc = ari_text.Encoder(int_base=base) ari = LiteralARI(value) loop = io.StringIO() enc.encode(ari, loop) LOGGER.info('Got text_dn: %s', loop.getvalue()) self.assertEqual(expect, loop.getvalue()) - def test_ari_text_encode_lit_prim_uint(self): TEST_CASE = [ (0, 10, "ari:0"), @@ -386,7 +385,7 @@ def test_ari_text_encode_lit_prim_uint(self): for row in TEST_CASE: value, base, expect = row with self.subTest(value): - enc = ari_text.Encoder(int_base = base) + enc = ari_text.Encoder(int_base=base) ari = LiteralARI(value) loop = io.StringIO() enc.encode(ari, loop) @@ -398,11 +397,11 @@ def test_ari_text_encode_lit_prim_float64(self): (1.1, 'f', "ari:1.100000"), (1.1, 'g', "ari:1.1"), (1.1e2, 'g', "ari:110.0"), - #(1.1e2, 'a', "ari:0x1.b8p+6"), # FIXME: %a not supported by ACE + # (1.1e2, 'a', "ari:0x1.b8p+6"), # FIXME: %a not supported by ACE # FIXME: (1.1e+10, 'g', "ari:1.1e+10"), (10.0, 'e', "ari:1.000000e+01"), - #(10, 'a', "ari:0x1.4p+3"), # FIXME: %a not supported by ACE - (float('nan'), ' ', "ari:NaN"), + # (10, 'a', "ari:0x1.4p+3"), # FIXME: %a not supported by ACE + (float('nan'), ' ', "ari:NaN"), (float('infinity'), ' ', "ari:Infinity"), (float('-infinity'), ' ', "ari:-Infinity"), ] @@ -410,7 +409,7 @@ def test_ari_text_encode_lit_prim_float64(self): for row in TEST_CASE: value, base, expect = row with self.subTest(value): - enc = ari_text.Encoder(float_form = base) + enc = ari_text.Encoder(float_form=base) ari = LiteralARI(value) loop = io.StringIO() enc.encode(ari, loop) @@ -461,16 +460,16 @@ def test_ari_text_encode_lit_prim_bstr(self): def test_ari_text_encode_objref_text(self): TEST_CASE = [ - ("adm", StructType.CONST, "hi", "ari://adm/CONST/hi"), - ("18", StructType.IDENT, "34", "ari://18/IDENT/34"), + ("example", "adm", StructType.CONST, "hi", "ari://example/adm/CONST/hi"), + (65535, 18, StructType.IDENT, "34", "ari://65535/18/IDENT/34"), ] for row in TEST_CASE: - ns_id, type_id, obj, expect = row + org_id, model_id, type_id, obj, expect = row with self.subTest(expect): enc = ari_text.Encoder() ari = ReferenceARI( - ident=Identity(ns_id, None, type_id, obj), + ident=Identity(org_id=org_id, model_id=model_id, type_id=type_id, obj_id=obj), params=None ) loop = io.StringIO() @@ -480,19 +479,19 @@ def test_ari_text_encode_objref_text(self): def test_ari_text_encode_nsref_text(self): TEST_CASE = [ - ("adm", "ari://adm/"), - ("example-adm-a@2024-06-25", "ari://example-adm-a@2024-06-25/"), - ("example-adm-a", "ari://example-adm-a/"), - ("!example-odm-b", "ari://!example-odm-b/"), - ("65536", "ari://65536/"), - ("-20", "ari://-20/"), + ("example", "adm", "ari://example/adm/"), + ("example", "adm-a@2024-06-25", "ari://example/adm-a@2024-06-25/"), + ("example", "adm-a", "ari://example/adm-a/"), + ("example", "!odm-b", "ari://example/!odm-b/"), + (65535, 0, "ari://65535/0/"), + (65535, -20, "ari://65535/-20/"), ] for row in TEST_CASE: - value, expect = row - with self.subTest(value): + org, model, expect = row + with self.subTest(f'{org}-{model}'): enc = ari_text.Encoder() ari = ReferenceARI( - ident=Identity(value, None, None), + ident=Identity(org_id=org, model_id=model), params=None ) loop = io.StringIO() @@ -555,7 +554,7 @@ def test_ari_text_decode_lit_prim_null(self): LOGGER.info('Got ARI %s', ari) self.assertIsInstance(ari, ARI) self.assertEqual(ari.value, None) - + def test_ari_text_decode_lit_prim_bool(self): TEST_CASE = [ ("false", False), @@ -719,7 +718,7 @@ def test_ari_text_decode_lit_prim_float64(self): self.assertIsInstance(ari, ARI) if math.isnan(expect): self.assertEqual(math.isnan(ari.value), True) - else: + else: self.assertEqual(ari.value, expect) def test_ari_text_decode_lit_typed_float32(self): @@ -746,7 +745,6 @@ def test_ari_text_decode_lit_typed_float32(self): self.assertIsInstance(ari, ARI) self.assertEqual(ari.value, expect) - def test_ari_text_decode_lit_typed_float64(self): TEST_CASE = [ # FIXME: ("ari:/REAL64/0", 0.0), @@ -889,11 +887,11 @@ def test_ari_text_decode_lit_typed_tp(self): ("ari:/TP/2000-01-01T00:00:20Z", datetime.datetime(2000, 1, 1, 0, 0, 20)), ("ari:/TP/20000101T000020Z", datetime.datetime(2000, 1, 1, 0, 0, 20)), # FIXME: datetime does not support nanoseconds - #("ari:/TP/20000101T000020.5Z", 20, 500e6), - #("ari:/TP/20.5", 20, 500e6), - #("ari:/TP/20.500", 20, 500e6), - #("ari:/TP/20.000001", 20, 1e3), - #("ari:/TP/20.000000001", 20, 1), + # ("ari:/TP/20000101T000020.5Z", 20, 500e6), + # ("ari:/TP/20.5", 20, 500e6), + # ("ari:/TP/20.500", 20, 500e6), + # ("ari:/TP/20.000001", 20, 1e3), + # ("ari:/TP/20.000000001", 20, 1), ] dec = ari_text.Decoder() @@ -910,10 +908,10 @@ def test_ari_text_decode_lit_typed_td(self): ("ari:/TD/PT1M", datetime.timedelta(seconds=60)), ("ari:/TD/PT20S", datetime.timedelta(seconds=20)), ("ari:/TD/PT20.5S", datetime.timedelta(seconds=20, microseconds=500000)), - ("ari:/TD/20.5", datetime.timedelta(seconds=20, microseconds=500000)), + ("ari:/TD/20.5", datetime.timedelta(seconds=20, microseconds=500000)), ("ari:/TD/20.500", datetime.timedelta(seconds=20, microseconds=500000)), ("ari:/TD/20.000001", datetime.timedelta(seconds=20, microseconds=1)), - ("ari:/TD/20.000000001", datetime.timedelta(seconds=20, microseconds=0)), # FIXME: nanonseconds not supported, truncates to 0 + ("ari:/TD/20.000000001", datetime.timedelta(seconds=20, microseconds=0)), # FIXME: nanonseconds not supported, truncates to 0 ("ari:/TD/+PT1M", datetime.timedelta(seconds=60, microseconds=0)), ("ari:/TD/-PT1M", datetime.timedelta(seconds=-60, microseconds=0)), ("ari:/TD/-P1DT", datetime.timedelta(seconds=-(24 * 60 * 60))), @@ -976,7 +974,7 @@ def test_ari_text_decode_lit_typed_tbl(self): dec = ari_text.Decoder() for row in TEST_CASE: text, expect_cols, expect_items = row - with self.subTest(text): #TODO: update loop + with self.subTest(text): # TODO: update loop ari = dec.decode(io.StringIO(text)) LOGGER.info('Got ARI %s', ari) self.assertIsInstance(ari, ARI) @@ -992,8 +990,8 @@ def test_ari_text_decode_lit_typed_execset(self): ("ari:/EXECSET/N=null;()", 0), ("ari:/EXECSET/N=0xabcd;()", 0), # FIXME: ("ari:/EXECSET/N=/UINT/0B0101;()", 0), - ("ari:/EXECSET/n=1234;(//test/CTRL/hi)", 1), - ("ari:/EXECSET/n=h'6869';(//test/CTRL/hi,//test/CTRL/eh)", 2), + ("ari:/EXECSET/n=1234;(//example/test/CTRL/hi)", 1), + ("ari:/EXECSET/n=h'6869';(//example/test/CTRL/hi,//example/test/CTRL/eh)", 2), ] dec = ari_text.Decoder() @@ -1008,12 +1006,12 @@ def test_ari_text_decode_lit_typed_execset(self): def test_ari_text_decode_lit_typed_rptset(self): TEST_CASE = [ # FIXME: ("ari:/RPTSET/n=null;r=725943845;", 0), #ARI_PRIM_NULL, 0), - ("ari:/RPTSET/n=1234;r=725943845;(t=0;s=//test/CTRL/hi;())", 1), #ARI_PRIM_INT64, 1), - ("ari:/RPTSET/n=1234;r=725943845;(t=0.0;s=//test/CTRL/hi;())", 1), #ARI_PRIM_INT64, 1), - # FIXME: ("ari:/RPTSET/n=1234;r=/TP/725943845;(t=/TD/0;s=//test/CTRL/hi;())", 1), #, ARI_PRIM_INT64, 1), - # FIXME: ("ari:/RPTSET/n=1234;r=/TP/725943845.000;(t=/TD/0;s=//test/CTRL/hi;())", 1), #, ARI_PRIM_INT64, 1), - # FIXME: ("ari:/RPTSET/n=1234;r=/TP/20230102T030405Z;(t=/TD/0;s=//test/CTRL/hi;())", 1), #, ARI_PRIM_INT64, 1), - # FIXME: ("ari:/RPTSET/n=h'6869';r=/TP/725943845;(t=/TD/0;s=//test/CTRL/hi;())(t=/TD/1;s=//test/CTRL/eh;())", 2), #ARI_PRIM_BSTR, 2), + ("ari:/RPTSET/n=1234;r=725943845;(t=0;s=//example/test/CTRL/hi;())", 1), # ARI_PRIM_INT64, 1), + ("ari:/RPTSET/n=1234;r=725943845;(t=0.0;s=//example/test/CTRL/hi;())", 1), # ARI_PRIM_INT64, 1), + # FIXME: ("ari:/RPTSET/n=1234;r=/TP/725943845;(t=/TD/0;s=//example/test/CTRL/hi;())", 1), #, ARI_PRIM_INT64, 1), + # FIXME: ("ari:/RPTSET/n=1234;r=/TP/725943845.000;(t=/TD/0;s=//example/test/CTRL/hi;())", 1), #, ARI_PRIM_INT64, 1), + # FIXME: ("ari:/RPTSET/n=1234;r=/TP/20230102T030405Z;(t=/TD/0;s=//example/test/CTRL/hi;())", 1), #, ARI_PRIM_INT64, 1), + # FIXME: ("ari:/RPTSET/n=h'6869';r=/TP/725943845;(t=/TD/0;s=//example/test/CTRL/hi;())(t=/TD/1;s=//example/test/CTRL/eh;())", 2), #ARI_PRIM_BSTR, 2), ] dec = ari_text.Decoder() @@ -1027,30 +1025,30 @@ def test_ari_text_decode_lit_typed_rptset(self): def test_ari_text_decode_objref(self): TEST_CASE = [ - ("ari://test/const/hi", StructType.CONST), - ("ari://test/ctrl/hi", StructType.CTRL), - ("ari://test/IDENT/hi", StructType.IDENT), - ("ari://test/TYPEDEF/hi", StructType.TYPEDEF), - ("ari://test/CONST/hi", StructType.CONST), - ("ari://test/VAR/hi", StructType.VAR), - ("ari://test/EDD/hi", StructType.EDD), - ("ari://test/CTRL/hi", StructType.CTRL), - ("ari://test/OPER/hi", StructType.OPER), - ("ari://test/SBR/hi", StructType.SBR), - ("ari://test/TBR/hi", StructType.TBR), - ("ari://test/ident/hi", StructType.IDENT), - ("ari://test/typedef/hi", StructType.TYPEDEF), - ("ari://test/const/hi", StructType.CONST), - ("ari://test/var/hi", StructType.VAR), - ("ari://test/edd/hi", StructType.EDD), - ("ari://test/ctrl/hi", StructType.CTRL), - ("ari://test/CtRl/hi", StructType.CTRL), - ("ari://test/oper/hi", StructType.OPER), - ("ari://test/sbr/hi", StructType.SBR), - ("ari://test/tbr/hi", StructType.TBR), - ("ari://adm/const/hi", StructType.CONST), - ("ari://adm/CONST/hi", StructType.CONST), - ("ari://adm/-2/hi", StructType.CONST), + ("ari://example/test/const/hi", StructType.CONST), + ("ari://example/test/ctrl/hi", StructType.CTRL), + ("ari://example/test/IDENT/hi", StructType.IDENT), + ("ari://example/test/TYPEDEF/hi", StructType.TYPEDEF), + ("ari://example/test/CONST/hi", StructType.CONST), + ("ari://example/test/VAR/hi", StructType.VAR), + ("ari://example/test/EDD/hi", StructType.EDD), + ("ari://example/test/CTRL/hi", StructType.CTRL), + ("ari://example/test/OPER/hi", StructType.OPER), + ("ari://example/test/SBR/hi", StructType.SBR), + ("ari://example/test/TBR/hi", StructType.TBR), + ("ari://example/test/ident/hi", StructType.IDENT), + ("ari://example/test/typedef/hi", StructType.TYPEDEF), + ("ari://example/test/const/hi", StructType.CONST), + ("ari://example/test/var/hi", StructType.VAR), + ("ari://example/test/edd/hi", StructType.EDD), + ("ari://example/test/ctrl/hi", StructType.CTRL), + ("ari://example/test/CtRl/hi", StructType.CTRL), + ("ari://example/test/oper/hi", StructType.OPER), + ("ari://example/test/sbr/hi", StructType.SBR), + ("ari://example/test/tbr/hi", StructType.TBR), + ("ari://example/adm/const/hi", StructType.CONST), + ("ari://example/adm/CONST/hi", StructType.CONST), + ("ari://example/adm/-2/hi", StructType.CONST), ] dec = ari_text.Decoder() @@ -1064,52 +1062,52 @@ def test_ari_text_decode_objref(self): def test_ari_text_decode_objref_invalid(self): TEST_CASE = [ - ("ari://test/LITERAL/hi"), - ("ari://test/NULL/hi"), - ("ari://test/BOOL/hi"), - ("ari://test/BYTE/hi"), - ("ari://test/INT/hi"), - ("ari://test/UINT/hi"), - ("ari://test/VAST/hi"), - ("ari://test/UVAST/hi"), - ("ari://test/REAL32/hi"), - ("ari://test/REAL64/hi"), - ("ari://test/TEXTSTR/hi"), - ("ari://test/BYTESTR/hi"), - ("ari://test/TP/hi"), - ("ari://test/TD/hi"), - ("ari://test/LABEL/hi"), - ("ari://test/CBOR/hi"), - ("ari://test/ARITYPE/hi"), - ("ari://test/AC/hi"), - ("ari://test/AM/hi"), - ("ari://test/TBL/hi"), - ("ari://test/EXECSET/hi"), - ("ari://test/RPTSET/hi"), - ("ari://test/OBJECT/hi"), - ("ari://test/literal/hi"), - ("ari://test/null/hi"), - ("ari://test/bool/hi"), - ("ari://test/byte/hi"), - ("ari://test/int/hi"), - ("ari://test/uint/hi"), - ("ari://test/vast/hi"), - ("ari://test/uvast/hi"), - ("ari://test/real32/hi"), - ("ari://test/real64/hi"), - ("ari://test/textstr/hi"), - ("ari://test/bytestr/hi"), - ("ari://test/tp/hi"), - ("ari://test/td/hi"), - ("ari://test/label/hi"), - ("ari://test/cbor/hi"), - ("ari://test/aritype/hi"), - ("ari://test/ac/hi"), - ("ari://test/am/hi"), - ("ari://test/tbl/hi"), - ("ari://test/execset/hi"), - ("ari://test/rptset/hi"), - ("ari://test/object/hi"), + ("ari://example/test/LITERAL/hi"), + ("ari://example/test/NULL/hi"), + ("ari://example/test/BOOL/hi"), + ("ari://example/test/BYTE/hi"), + ("ari://example/test/INT/hi"), + ("ari://example/test/UINT/hi"), + ("ari://example/test/VAST/hi"), + ("ari://example/test/UVAST/hi"), + ("ari://example/test/REAL32/hi"), + ("ari://example/test/REAL64/hi"), + ("ari://example/test/TEXTSTR/hi"), + ("ari://example/test/BYTESTR/hi"), + ("ari://example/test/TP/hi"), + ("ari://example/test/TD/hi"), + ("ari://example/test/LABEL/hi"), + ("ari://example/test/CBOR/hi"), + ("ari://example/test/ARITYPE/hi"), + ("ari://example/test/AC/hi"), + ("ari://example/test/AM/hi"), + ("ari://example/test/TBL/hi"), + ("ari://example/test/EXECSET/hi"), + ("ari://example/test/RPTSET/hi"), + ("ari://example/test/OBJECT/hi"), + ("ari://example/test/literal/hi"), + ("ari://example/test/null/hi"), + ("ari://example/test/bool/hi"), + ("ari://example/test/byte/hi"), + ("ari://example/test/int/hi"), + ("ari://example/test/uint/hi"), + ("ari://example/test/vast/hi"), + ("ari://example/test/uvast/hi"), + ("ari://example/test/real32/hi"), + ("ari://example/test/real64/hi"), + ("ari://example/test/textstr/hi"), + ("ari://example/test/bytestr/hi"), + ("ari://example/test/tp/hi"), + ("ari://example/test/td/hi"), + ("ari://example/test/label/hi"), + ("ari://example/test/cbor/hi"), + ("ari://example/test/aritype/hi"), + ("ari://example/test/ac/hi"), + ("ari://example/test/am/hi"), + ("ari://example/test/tbl/hi"), + ("ari://example/test/execset/hi"), + ("ari://example/test/rptset/hi"), + ("ari://example/test/object/hi"), ] dec = ari_text.Decoder() @@ -1122,15 +1120,15 @@ def test_ari_text_decode_objref_invalid(self): def test_ari_text_decode_nsref(self): TEST_CASE = [ - ("ari://adm"), - ("ari://adm/"), - ("ari://18"), - # FIXME: ("ari://18/"), - ("ari://65536/"), - ("ari://-20/"), - ("ari://example-adm-a@2024-06-25/"), - ("ari://example-adm-a/"), - ("ari://!example-odm-b/"), + ("ari://example/adm"), + ("ari://example/adm/"), + ("ari://65535/18"), + # ("ari://65535/18/"), + ("ari://65535/-20/"), + # ("ari://-10/20/"), + ("ari://example/adm-a@2024-06-25/"), + ("ari://example/adm-a/"), + ("ari://example/!odm-b/"), ] dec = ari_text.Decoder() @@ -1147,24 +1145,26 @@ def test_ari_text_decode_nsref(self): def test_ari_text_decode_ariref(self): TEST_CASE = [ - ("ari:./CTRL/do_thing", StructType.CTRL), #TODO: update values - ("ari:./CTRL/otherobj(%22a%20param%22,/UINT/10)", StructType.CTRL), - ("ari:./-2/30", StructType.CONST), - ("./CTRL/do_thing", StructType.CTRL), - ("./CTRL/otherobj(%22a%20param%22,/UINT/10)", StructType.CTRL), - ("./-2/30", StructType.CONST), + ("ari:./CTRL/do_thing", None, StructType.CTRL), # TODO: update values + ("ari:../adm/CTRL/do_thing", 'adm', StructType.CTRL), # TODO: update values + ("ari:./CTRL/otherobj(%22a%20param%22,/UINT/10)", None, StructType.CTRL), + ("ari:./-2/30", None, StructType.CONST), + ("./CTRL/do_thing", None, StructType.CTRL), + ("./CTRL/otherobj(%22a%20param%22,/UINT/10)", None, StructType.CTRL), + ("./-2/30", None, StructType.CONST), ] dec = ari_text.Decoder() for row in TEST_CASE: - text, expect = row + text, expect_mod, expect_typ = row with self.subTest(text): ari = dec.decode(io.StringIO(text)) LOGGER.info('Got ARI %s', ari) self.assertIsInstance(ari, ARI) self.assertIsInstance(ari, ReferenceARI) - self.assertEqual(ari.ident.ns_id, None) - self.assertEqual(ari.ident.type_id, expect) + self.assertEqual(ari.ident.org_id, None) + self.assertEqual(ari.ident.model_id, expect_mod) + self.assertEqual(ari.ident.type_id, expect_typ) def test_ari_text_loopback(self): TEST_CASE = [ @@ -1205,15 +1205,15 @@ def test_ari_text_loopback(self): ("ari:/TBL/c=0;"), ("ari:/TBL/c=1;"), ("ari:/EXECSET/n=null;()"), - ("ari:/EXECSET/n=1234;(//test/CTRL/hi)"), - # FIXME: ("ari:/EXECSET/n=h'6869';(//test/CTRL/hi,//test/CTRL/eh)"), - # FIXME: ("ari:/RPTSET/n=1234;r=/TP/20000101T001640Z;(t=/TD/PT0S;s=//test/CTRL/hi;(null,3,h'6869'))"), - # FIXME: ("ari:/RPTSET/n=1234;r=/TP/20230102T030405Z;(t=/TD/PT0S;s=//test/CTRL/hi;(null,3,h'6869'))"), - ("ari://test/CONST/that"), - # FIXME: ("ari://test@1234/CONST/that"), - ("ari://!test/CONST/that"), - ("ari://test/CTRL/that(34)"), - ("ari://2/CTRL/4(hi)"), + ("ari:/EXECSET/n=1234;(//example/test/CTRL/hi)"), + # FIXME: ("ari:/EXECSET/n=h'6869';(//example/test/CTRL/hi,//example/test/CTRL/eh)"), + # FIXME: ("ari:/RPTSET/n=1234;r=/TP/20000101T001640Z;(t=/TD/PT0S;s=//example/test/CTRL/hi;(null,3,h'6869'))"), + # FIXME: ("ari:/RPTSET/n=1234;r=/TP/20230102T030405Z;(t=/TD/PT0S;s=//example/test/CTRL/hi;(null,3,h'6869'))"), + ("ari://example/test/CONST/that"), + # FIXME: ("ari://example/test@1234/CONST/that"), + ("ari://example/!test/CONST/that"), + ("ari://example/test/CTRL/that(34)"), + ("ari://65535/2/CTRL/4(hi)"), # FIXME: ("./CTRL/do_thing"), # FIXME: ("ari:/CBOR/h'0A'"), # FIXME: ("ari:/CBOR/h'A164746573748203F94480'"), @@ -1250,27 +1250,27 @@ def test_ari_text_reencode(self): ("ari:/tbl/c=3;(1,2,3)", "ari:/TBL/c=3;(1,2,3)"), ("ari:/execset/n=null;()", "ari:/EXECSET/n=null;()"), # FIXME: - #("ari:/rptset/n=1234;r=1000;(t=0;s=//test/ctrl/hi;(null,3,h'6869'))", - # "ari:/RPTSET/n=1234;r=/TP/20000101T001640Z;(t=/TD/PT0S;s=//test/CTRL/hi;(null,3,h'6869'))"), - #("ari:/rptset/n=1234;r=/TP/1000;(t=/TD/0;s=//test/CTRL/hi;(null,3,h'6869'))", - # "ari:/RPTSET/n=1234;r=/TP/20000101T001640Z;(t=/TD/PT0S;s=//test/CTRL/hi;(null,3,h'6869'))"), - #("ari:/rptset/n=1234;r=/TP/1000;(t=0;s=//test/CTRL/hi;(null,3,h'6869'))", - # "ari:/RPTSET/n=1234;r=/TP/20000101T001640Z;(t=/TD/PT0S;s=//test/CTRL/hi;(null,3,h'6869'))"), - #("ari:/rptset/n=1234;r=/TP/1000;(t=100.5;s=//test/CTRL/hi;(null,3,h'6869'))", - # "ari:/RPTSET/n=1234;r=/TP/20000101T001640Z;(t=/TD/PT1M40.5S;s=//test/CTRL/hi;(null,3,h'6869'))"), - #("ari:/rptset/n=1234;r=1000;(t=/TD/0;s=//test/CTRL/hi;(null,3,h'6869'))", - # "ari:/RPTSET/n=1234;r=/TP/20000101T001640Z;(t=/TD/PT0S;s=//test/CTRL/hi;(null,3,h'6869'))"), - #("ari:/rptset/n=1234;r=1000.0;(t=/TD/0;s=//test/CTRL/hi;(null,3,h'6869'))", - # "ari:/RPTSET/n=1234;r=/TP/20000101T001640Z;(t=/TD/PT0S;s=//test/CTRL/hi;(null,3,h'6869'))"), - #("ari:/rptset/n=1234;r=/UVAST/1000;(t=/TD/0;s=//test/CTRL/hi;(null,3,h'6869'))", - # "ari:/RPTSET/n=1234;r=/TP/20000101T001640Z;(t=/TD/PT0S;s=//test/CTRL/hi;(null,3,h'6869'))"), - #("ari:/rptset/n=1234;r=/UVAST/0b1000;(t=/TD/0;s=//test/CTRL/hi;(null,3,h'6869'))", - # "ari:/RPTSET/n=1234;r=/TP/20000101T000008Z;(t=/TD/PT0S;s=//test/CTRL/hi;(null,3,h'6869'))"), - #("ari:/rptset/n=1234;r=/TP/1000.987654321;(t=/TD/0;s=//test/CTRL/hi;(null,3,h'6869'))", - # "ari:/RPTSET/n=1234;r=/TP/20000101T001640.987654321Z;(t=/TD/PT0S;s=//test/CTRL/hi;(null,3,h'6869'))"), - #("ari:/rptset/n=1234;r=1000.9876543210987654321;(t=/TD/0;s=//test/CTRL/hi;(null,3,h'6869'))", - # "ari:/RPTSET/n=1234;r=/TP/20000101T001640.987654321Z;(t=/TD/PT0S;s=//test/CTRL/hi;(null,3,h'6869'))"), - ("ari://test", "ari://test/"), + # ("ari:/rptset/n=1234;r=1000;(t=0;s=//example/test/ctrl/hi;(null,3,h'6869'))", + # "ari:/RPTSET/n=1234;r=/TP/20000101T001640Z;(t=/TD/PT0S;s=//example/test/CTRL/hi;(null,3,h'6869'))"), + # ("ari:/rptset/n=1234;r=/TP/1000;(t=/TD/0;s=//example/test/CTRL/hi;(null,3,h'6869'))", + # "ari:/RPTSET/n=1234;r=/TP/20000101T001640Z;(t=/TD/PT0S;s=//example/test/CTRL/hi;(null,3,h'6869'))"), + # ("ari:/rptset/n=1234;r=/TP/1000;(t=0;s=//example/test/CTRL/hi;(null,3,h'6869'))", + # "ari:/RPTSET/n=1234;r=/TP/20000101T001640Z;(t=/TD/PT0S;s=//example/test/CTRL/hi;(null,3,h'6869'))"), + # ("ari:/rptset/n=1234;r=/TP/1000;(t=100.5;s=//example/test/CTRL/hi;(null,3,h'6869'))", + # "ari:/RPTSET/n=1234;r=/TP/20000101T001640Z;(t=/TD/PT1M40.5S;s=//example/test/CTRL/hi;(null,3,h'6869'))"), + # ("ari:/rptset/n=1234;r=1000;(t=/TD/0;s=//example/test/CTRL/hi;(null,3,h'6869'))", + # "ari:/RPTSET/n=1234;r=/TP/20000101T001640Z;(t=/TD/PT0S;s=//example/test/CTRL/hi;(null,3,h'6869'))"), + # ("ari:/rptset/n=1234;r=1000.0;(t=/TD/0;s=//example/test/CTRL/hi;(null,3,h'6869'))", + # "ari:/RPTSET/n=1234;r=/TP/20000101T001640Z;(t=/TD/PT0S;s=//example/test/CTRL/hi;(null,3,h'6869'))"), + # ("ari:/rptset/n=1234;r=/UVAST/1000;(t=/TD/0;s=//example/test/CTRL/hi;(null,3,h'6869'))", + # "ari:/RPTSET/n=1234;r=/TP/20000101T001640Z;(t=/TD/PT0S;s=//example/test/CTRL/hi;(null,3,h'6869'))"), + # ("ari:/rptset/n=1234;r=/UVAST/0b1000;(t=/TD/0;s=//example/test/CTRL/hi;(null,3,h'6869'))", + # "ari:/RPTSET/n=1234;r=/TP/20000101T000008Z;(t=/TD/PT0S;s=//example/test/CTRL/hi;(null,3,h'6869'))"), + # ("ari:/rptset/n=1234;r=/TP/1000.987654321;(t=/TD/0;s=//example/test/CTRL/hi;(null,3,h'6869'))", + # "ari:/RPTSET/n=1234;r=/TP/20000101T001640.987654321Z;(t=/TD/PT0S;s=//example/test/CTRL/hi;(null,3,h'6869'))"), + # ("ari:/rptset/n=1234;r=1000.9876543210987654321;(t=/TD/0;s=//example/test/CTRL/hi;(null,3,h'6869'))", + # "ari:/RPTSET/n=1234;r=/TP/20000101T001640.987654321Z;(t=/TD/PT0S;s=//example/test/CTRL/hi;(null,3,h'6869'))"), + ("ari://example/test", "ari://example/test/"), # FIXME: ("ari:./ctrl/hi", "./CTRL/hi"), ] diff --git a/tests/test_lookup.py b/tests/test_lookup.py index 1593f7b..02efb16 100644 --- a/tests/test_lookup.py +++ b/tests/test_lookup.py @@ -108,7 +108,7 @@ def _process(self, text:str) -> lookup.ActualParameterSet: return lookup.ActualParameterSet(ref.params, self._fparams[ref.ident.obj_id]) def test_params_none(self): - aparams = self._process('//example-test-mod/EDD/many_params') + aparams = self._process('//example/test-mod/EDD/many_params') self.assertEqual( [ ari.UNDEFINED, @@ -119,7 +119,7 @@ def test_params_none(self): ) def test_params_empty(self): - aparams = self._process('//example-test-mod/EDD/many_params()') + aparams = self._process('//example/test-mod/EDD/many_params()') self.assertEqual( [ ari.UNDEFINED, @@ -130,7 +130,7 @@ def test_params_empty(self): ) def test_params_list(self): - aparams = self._process('//example-test-mod/EDD/many_params(1,2)') + aparams = self._process('//example/test-mod/EDD/many_params(1,2)') self.assertEqual( [ ari.LiteralARI(1, ari.StructType.INT), @@ -141,7 +141,7 @@ def test_params_list(self): ) def test_params_map_ord(self): - aparams = self._process('//example-test-mod/EDD/many_params(0=1,2=3)') + aparams = self._process('//example/test-mod/EDD/many_params(0=1,2=3)') self.assertEqual( [ ari.LiteralARI(1, ari.StructType.INT), @@ -152,7 +152,7 @@ def test_params_map_ord(self): ) def test_params_map_name(self): - aparams = self._process('//example-test-mod/EDD/many_params(one=1,three=3)') + aparams = self._process('//example/test-mod/EDD/many_params(one=1,three=3)') self.assertEqual( [ ari.LiteralARI(1, ari.StructType.INT), @@ -163,7 +163,7 @@ def test_params_map_name(self): ) def test_params_seq_vals(self): - aparams = self._process('//example-test-mod/EDD/greedy_param(1,2,3,4,5)') + aparams = self._process('//example/test-mod/EDD/greedy_param(1,2,3,4,5)') self.assertEqual( [ ari.LiteralARI(1, ari.StructType.INT), @@ -178,7 +178,7 @@ def test_params_seq_vals(self): ) def test_params_seq_empty(self): - aparams = self._process('//example-test-mod/EDD/greedy_param()') + aparams = self._process('//example/test-mod/EDD/greedy_param()') self.assertEqual( [ ari.UNDEFINED, @@ -188,7 +188,7 @@ def test_params_seq_empty(self): ) def test_params_seq_frommap(self): - aparams = self._process('//example-test-mod/EDD/greedy_param(0=1,1=/AC/(2,3))') + aparams = self._process('//example/test-mod/EDD/greedy_param(0=1,1=/AC/(2,3))') self.assertEqual( [ ari.LiteralARI(1, ari.StructType.INT), From 273ce47ecef3914938fa662d6fd6fe77fa81051d Mon Sep 17 00:00:00 2001 From: Brian Sipos Date: Fri, 31 Jan 2025 11:57:31 -0500 Subject: [PATCH 2/3] Fix remaining behavior and tests --- src/ace/adm_set.py | 4 +- src/ace/ari.py | 4 +- src/ace/constraints/basic.py | 32 +++++++------- src/ace/constraints/core.py | 25 +++++------ src/ace/nickname.py | 40 +++++++++++++---- src/ace/typing.py | 22 +++++----- tests/test_constraints.py | 85 +++++++++++++++++++++--------------- tests/test_models.py | 11 +++-- tests/test_nickname.py | 41 ++++++++--------- tests/test_typing.py | 8 ++-- 10 files changed, 158 insertions(+), 114 deletions(-) diff --git a/src/ace/adm_set.py b/src/ace/adm_set.py index ac913e0..d220898 100644 --- a/src/ace/adm_set.py +++ b/src/ace/adm_set.py @@ -390,8 +390,8 @@ def _post_load(self, adm_new:models.AdmModule, del_dupe:bool): # if dependant adm not added yet import_names = [obj.name for obj in adm_new.imports] pending = False - for adm_name in import_names: - if not adm_name in self: + for module_name in import_names: + if not module_name in self: pending = True break diff --git a/src/ace/ari.py b/src/ace/ari.py index 4fb2921..95599f6 100644 --- a/src/ace/ari.py +++ b/src/ace/ari.py @@ -316,8 +316,8 @@ def __str__(self) -> str: text += f'/{self.org_id}' if self.model_id is not None: text += f'/{self.model_id}' - if self.ns_rev: - text += f'@{self.ns_rev}' + if self.model_rev: + text += f'@{self.model_rev}' text += f'/{self.type_id.name}' text += f'/{self.obj_id}' return text diff --git a/src/ace/constraints/basic.py b/src/ace/constraints/basic.py index a516695..ac4b446 100644 --- a/src/ace/constraints/basic.py +++ b/src/ace/constraints/basic.py @@ -59,23 +59,23 @@ class unique_adm_names: # pylint: disable=invalid-name def __call__(self, issuelist, obj, db_sess): count = 0 - for name in ('norm_name', 'enum'): - attr = getattr(models.AdmModule, name) - search = ( - db_sess.query(attr, func.count(models.AdmModule.id)) - .group_by(attr) - .having(func.count(models.AdmModule.id) > 1) + attr = models.AdmModule.norm_name + + search = ( + db_sess.query(attr, func.count(models.AdmModule.id)) + .group_by(attr) + .having(func.count(models.AdmModule.id) > 1) + ) + for row in search.all(): + query = db_sess.query(models.AdmModule).filter( + attr == row[0] ) - for row in search.all(): - query = db_sess.query(models.AdmModule).filter( - attr == row[0] - ) - for adm in query.all(): - issuelist.append(Issue( - obj=adm, - detail=f'Multiple ADMs with metadata "{name}" of "{row[0]}"' - )) - count += 1 + for adm in query.all(): + issuelist.append(Issue( + obj=adm, + detail=f'Multiple ADMs with metadata "norm_name" of "{row[0]}"' + )) + count += 1 return count diff --git a/src/ace/constraints/core.py b/src/ace/constraints/core.py index f34fa31..029886a 100644 --- a/src/ace/constraints/core.py +++ b/src/ace/constraints/core.py @@ -26,24 +26,23 @@ import logging from ace import models - LOGGER = logging.getLogger(__name__) -#: Accumulated list of all constraints to check CONSTRAINTS = {} +''' Accumulated list of all constraints to check ''' @dataclass class Issue: ''' An issue resulting from a failed constraint. ''' - #: The name of the constraint noting the issue, which will be set automatically check_name: str = None - #: The name of the ADM containing the issue, which will be set automatically - adm_name: str = None - #: The object containing the issue + ''' The name of the constraint noting the issue, which will be set automatically ''' + module_name: str = None + ''' The name of the ADM module containing the issue, which will be set automatically ''' obj: object = None - #: Any specific detail about the issue + ''' The object containing the issue ''' detail: str = None + ''' Any specific detail about the issue ''' def register(obj): @@ -74,7 +73,7 @@ class Checker: def __init__(self, db_sess): self._db_sess = db_sess - def check(self, src: models.AdmModule = None): + def check(self, src: models.AdmModule=None): ''' Check a specific ADM for issues. :param src: The ADM to check or None. @@ -97,8 +96,8 @@ def check(self, src: models.AdmModule = None): # Run non-global constraints per each adm for adm in adm_list: - adm_name = adm.norm_name - LOGGER.debug('Checking ADM: %s', adm_name) + module_name = adm.norm_name + LOGGER.debug('Checking ADM: %s', module_name) for cst_name, cst in CONSTRAINTS.items(): if getattr(cst, 'is_global', False): continue @@ -117,11 +116,11 @@ def _add_result(self, issuelist, check_count, cst_name, cst, adm): check_count += count for issue in issuelist: - if issue.adm_name is None: + if issue.module_name is None: if adm is not None: - issue.adm_name = adm.norm_name + issue.module_name = adm.norm_name elif isinstance(issue.obj, models.AdmModule): - issue.adm_name = issue.obj.norm_name + issue.module_name = issue.obj.norm_name if issue.check_name is None: issue.check_name = cst_name LOGGER.debug( diff --git a/src/ace/nickname.py b/src/ace/nickname.py index 536dc4a..333342a 100644 --- a/src/ace/nickname.py +++ b/src/ace/nickname.py @@ -77,17 +77,29 @@ def _convert_ref(self, ari:ReferenceARI) -> ReferenceARI: if self._mode == Mode.TO_NN: # Prefer nicknames - ns_id = ari.ident.ns_id - if adm is None or adm.enum is None: + org_id = ari.ident.org_id + if adm is None or adm.ns_org_enum is None: if self._must: if adm is None: err = 'does not exist' else: err = 'does not have an enumeration' - msg = f'The ADM named {ns_id} {err}' + msg = f'The ADM named {org_id} {err}' raise RuntimeError(msg) else: - ns_id = adm.enum + org_id = adm.ns_org_enum + + model_id = ari.ident.model_id + if adm is None or adm.ns_model_enum is None: + if self._must: + if adm is None: + err = 'does not exist' + else: + err = 'does not have an enumeration' + msg = f'The ADM named {model_id} {err}' + raise RuntimeError(msg) + else: + model_id = adm.ns_model_enum obj_id = ari.ident.obj_id if obj is None or obj.enum is None: @@ -103,19 +115,28 @@ def _convert_ref(self, ari:ReferenceARI) -> ReferenceARI: # ARI IDs from enums new_ident = Identity( - ns_id=ns_id, + org_id=org_id, + model_id=model_id, type_id=ari.ident.type_id, obj_id=obj_id ) elif self._mode == Mode.FROM_NN: - ns_id = ari.ident.ns_id + org_id = ari.ident.org_id + if adm is None: + if self._must: + msg = f'The ADM organization named {org_id} does not exist' + raise RuntimeError(msg) + else: + org_id = adm.ns_org_name + + model_id = ari.ident.model_id if adm is None: if self._must: - msg = f'The ADM named {ns_id} does not exist' + msg = f'The ADM model named {model_id} does not exist' raise RuntimeError(msg) else: - ns_id = adm.norm_name + model_id = adm.ns_model_name obj_id = ari.ident.obj_id if obj is None: @@ -127,7 +148,8 @@ def _convert_ref(self, ari:ReferenceARI) -> ReferenceARI: # ARI IDs from names new_ident = Identity( - ns_id=ns_id, + org_id=org_id, + model_id=model_id, type_id=ari.ident.type_id, obj_id=obj_id ) diff --git a/src/ace/typing.py b/src/ace/typing.py index b353013..34bdc47 100644 --- a/src/ace/typing.py +++ b/src/ace/typing.py @@ -36,8 +36,10 @@ LOGGER = logging.getLogger(__name__) -AMM_MODULE_ID = 'ietf-amm' -''' Identifier of the ietf-amm module. ''' + +def get_amm_ident(obj_id:str) -> Identity: + ''' Get an IDENT in the ietf-amm model. ''' + return Identity(org_id='ietf', model_id='amm', type_id=StructType.IDENT, obj_id=obj_id) class Constraint: @@ -498,7 +500,7 @@ def all_constraints(self) -> Set[Constraint]: def ari_name(self) -> ARI: return ReferenceARI( - Identity(ns_id=AMM_MODULE_ID, type_id=StructType.IDENT, obj_id='semtype-use'), + get_amm_ident('semtype-use'), params={ LiteralARI('name'): self.type_ari, } @@ -558,7 +560,7 @@ def all_constraints(self) -> Set[Constraint]: def ari_name(self) -> ARI: return ReferenceARI( - Identity(ns_id=AMM_MODULE_ID, type_id=StructType.IDENT, obj_id='semtype-union'), + get_amm_ident('semtype-union'), params={ LiteralARI('choices'): LiteralARI([choice.ari_name() for choice in self.types], StructType.AC), } @@ -616,7 +618,7 @@ def all_type_ids(self) -> Set[StructType]: def ari_name(self) -> ARI: return ReferenceARI( - Identity(ns_id=AMM_MODULE_ID, type_id=StructType.IDENT, obj_id='semtype-ulist'), + get_amm_ident('semtype-ulist'), params={ LiteralARI('item-type'): self.base.ari_name(), LiteralARI('min-elements'): LiteralARI(self.min_elements), @@ -697,7 +699,7 @@ def all_type_ids(self) -> Set[StructType]: def ari_name(self) -> ARI: return ReferenceARI( - Identity(ns_id=AMM_MODULE_ID, type_id=StructType.IDENT, obj_id='semtype-seq'), + get_amm_ident('semtype-seq'), params={ LiteralARI('item-type'): self.base.ari_name(), LiteralARI('min-elements'): LiteralARI(self.min_elements), @@ -758,7 +760,7 @@ def all_type_ids(self) -> Set[StructType]: def ari_name(self) -> ARI: return ReferenceARI( - Identity(ns_id=AMM_MODULE_ID, type_id=StructType.IDENT, obj_id='semtype-dlist'), + get_amm_ident('semtype-dlist'), params={ LiteralARI('item-types'): LiteralARI([part.ari_name() for part in self.parts], StructType.AC), } @@ -846,7 +848,7 @@ def all_type_ids(self) -> Set[StructType]: def ari_name(self) -> ARI: return ReferenceARI( - Identity(ns_id=AMM_MODULE_ID, type_id=StructType.IDENT, obj_id='semtype-umap'), + get_amm_ident('semtype-umap'), params={ LiteralARI('key-type'): self.kbase.ari_name() if self.kbase is not None else NULL, LiteralARI('value-type'): self.kbase.ari_name() if self.kbase is not None else NULL, @@ -907,7 +909,7 @@ class TableColumn: def ari_name(self) -> ARI: return ReferenceARI( - Identity(ns_id=AMM_MODULE_ID, type_id=StructType.IDENT, obj_id='semtype-tblt-col'), + get_amm_ident('semtype-tblt-col'), params={ LiteralARI('name'): LiteralARI('name'), LiteralARI('datatype'): self.base.ari_name(), @@ -940,7 +942,7 @@ def all_type_ids(self) -> Set[StructType]: def ari_name(self) -> ARI: return ReferenceARI( - Identity(ns_id=AMM_MODULE_ID, type_id=StructType.IDENT, obj_id='semtype-tblt'), + get_amm_ident('semtype-tblt'), params={ LiteralARI('columns'): LiteralARI([col.ari_name() for col in self.columns], StructType.AC), LiteralARI('min-elements'): LiteralARI(self.min_elements), diff --git a/tests/test_constraints.py b/tests/test_constraints.py index 784e678..cac4b59 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -53,8 +53,8 @@ def tearDown(self): models.Base.metadata.drop_all(self._db_eng) self._db_eng = None - def assertIssuePattern(self, issue: constraints.Issue, adm_name, check_name, obj_ref, detail_re): - self.assertEqual(adm_name, issue.adm_name) + def assertIssuePattern(self, issue: constraints.Issue, module_name, check_name, obj_ref, detail_re): + self.assertEqual(module_name, issue.module_name) self.assertEqual(check_name, issue.check_name) self.assertEqual(obj_ref, issue.obj) self.assertRegex(issue.detail, detail_re) @@ -62,16 +62,19 @@ def assertIssuePattern(self, issue: constraints.Issue, adm_name, check_name, obj def _from_text(self, text:str) -> ari.ARI: return self._ari_dec.decode(io.StringIO(text)) - def _add_mod(self, abs_file_path, name, enum): + def _add_mod(self, abs_file_path, org_name, org_enum, model_name, model_enum): src = models.AdmSource( abs_file_path=abs_file_path, file_text='', ) adm = models.AdmModule( source=src, - name=name, - norm_name=name, - enum=enum, + module_name=f'{org_name}-{model_name}', + norm_name=f'{org_name}-{model_name}', + ns_org_name=org_name, + ns_org_enum=org_enum, + ns_model_name=model_name, + ns_model_enum=model_enum, metadata_list=models.MetadataList(), ) adm.revisions = [ @@ -88,8 +91,10 @@ class TestConstraintsBasic(BaseTest): def test_file_name(self): adm = self._add_mod( abs_file_path='othername.yang', - name='myadm', - enum=200, + org_name='example', + org_enum=65535, + model_name='myadm', + model_enum=200, ) self._db_sess.commit() @@ -99,7 +104,7 @@ def test_file_name(self): self.assertEqual(1, len(issues), msg=issues) self.assertIssuePattern( issues[0], - adm_name='myadm', + module_name='example-myadm', check_name='ace.constraints.basic.same_file_name', obj_ref=adm, detail_re=r'different', @@ -107,15 +112,19 @@ def test_file_name(self): def test_duplicate_adm_names(self): adm_a = self._add_mod( - abs_file_path='dir-a/myadm.yang', - name='myadm', - enum=200, + abs_file_path='dir-a/example-myadm.yang', + org_name='example', + org_enum=65535, + model_name='myadm', + model_enum=200, ) adm_b = self._add_mod( - abs_file_path='dir-b/myadm.yang', - name='myadm', - enum=201, + abs_file_path='dir-b/example-myadm.yang', + org_name='example', + org_enum=65535, + model_name='myadm', + model_enum=201, ) self._db_sess.commit() @@ -125,17 +134,19 @@ def test_duplicate_adm_names(self): self.assertEqual(2, len(issues), msg=issues) self.assertIssuePattern( issues[0], - adm_name='myadm', + module_name='example-myadm', check_name='ace.constraints.basic.unique_adm_names', obj_ref=adm_a, - detail_re=r'Multiple ADMs with metadata "norm_name" of "myadm"', + detail_re=r'Multiple ADMs with metadata "norm_name" of "example-myadm"', ) def test_duplicate_object_names(self): adm = self._add_mod( - abs_file_path='myadm.yang', - name='myadm', - enum=200, + abs_file_path='example-myadm.yang', + org_name='example', + org_enum=65535, + model_name='myadm', + model_enum=200, ) adm.ctrl.append(models.Ctrl(name='control_a', norm_name='control_a')) adm.ctrl.append(models.Ctrl(name='control_a', norm_name='control_a')) @@ -147,7 +158,7 @@ def test_duplicate_object_names(self): self.assertEqual(1, len(issues), msg=issues) self.assertIssuePattern( issues[0], - adm_name='myadm', + module_name='example-myadm', check_name='ace.constraints.basic.unique_object_names', obj_ref=adm.ctrl[-1], detail_re=r'duplicate', @@ -155,9 +166,11 @@ def test_duplicate_object_names(self): def test_valid_type_name(self): adm = self._add_mod( - abs_file_path='myadm.yang', - name='myadm', - enum=200, + abs_file_path='example-myadm.yang', + org_name='example', + org_enum=65535, + model_name='myadm', + model_enum=200, ) val = 'ari:/INT/10' adm.var.append(models.Var( @@ -174,7 +187,7 @@ def test_valid_type_name(self): self.assertEqual(1, len(issues), msg=issues) self.assertIssuePattern( issues[0], - adm_name='myadm', + module_name='example-myadm', check_name='ace.constraints.basic.valid_type_name', obj_ref=adm.var[0], detail_re=r'Within the object named "someval" the type names are not known', @@ -182,20 +195,24 @@ def test_valid_type_name(self): def test_valid_reference_ari(self): adm_a = self._add_mod( - abs_file_path='adm_a.yang', - name='adm_a', - enum=200, + abs_file_path='example-adm-a.yang', + org_name='example', + org_enum=65535, + model_name='adm-a', + model_enum=200, ) adm_a.ctrl.append(models.Ctrl(name='control_a', norm_name='control_a')) adm_b = self._add_mod( - abs_file_path='adm_b.yang', - name='adm_b', - enum=201, + abs_file_path='example-adm-b.yang', + org_name='example', + org_enum=65535, + model_name='adm-b', + model_enum=201, ) - val = 'ari:/AC/(//adm_a/CTRL/control_a,//adm_a/CTRL/control_c,//adm_c/CTRL/control_a)' + val = 'ari:/AC/(//example/adm-a/CTRL/control_a,//example/adm-a/CTRL/control_c,//example/adm-c/CTRL/control_a)' adm_b.const.append(models.Const( name='macro', typeobj=typing.TypeUse(type_ari=ari.LiteralARI(ari.StructType.AC, ari.StructType.ARITYPE)), @@ -215,14 +232,14 @@ def test_valid_reference_ari(self): self.assertEqual(2, len(issues), msg=issues) self.assertIssuePattern( issues[0], - adm_name='adm_b', + module_name='example-adm-b', check_name='ace.constraints.basic.valid_reference_ari', obj_ref=adm_b.const[0], detail_re=r'Within the object named "macro" the reference ARI for .*\bcontrol_c\b.* is not resolvable', ) self.assertIssuePattern( issues[1], - adm_name='adm_b', + module_name='example-adm-b', check_name='ace.constraints.basic.valid_reference_ari', obj_ref=adm_b.const[0], detail_re=r'Within the object named "macro" the reference ARI for .*\bcontrol_a\b.* is not resolvable', diff --git a/tests/test_models.py b/tests/test_models.py index b372619..ead0748 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -44,12 +44,15 @@ def tearDown(self): def test_simple(self): src = models.AdmSource( - abs_file_path='hi', + abs_file_path='example-hi', ) mod = models.AdmModule( source=src, - norm_name='hi', - enum=10, + norm_name='example-hi', + ns_org_name="example", + ns_org_enum=65535, + ns_model_name="hi", + ns_model_enum=10, metadata_list=models.MetadataList(), ) self._db_sess.add_all([src, mod]) @@ -58,4 +61,4 @@ def test_simple(self): objs = self._db_sess.query(models.AdmModule) self.assertEqual(1, objs.count()) adm = objs.first() - self.assertEqual('hi', adm.source.abs_file_path) + self.assertEqual('example-hi', adm.source.abs_file_path) diff --git a/tests/test_nickname.py b/tests/test_nickname.py index 5fd89b4..7190544 100644 --- a/tests/test_nickname.py +++ b/tests/test_nickname.py @@ -22,11 +22,10 @@ # ''' Verify behavior of the :mod:`ace.adm_yang` module tree. ''' -import io import logging import os import unittest -from ace import ari, ari_text, nickname +from ace import ari, nickname from .test_adm_yang import BaseYang LOGGER = logging.getLogger(__name__) @@ -35,6 +34,8 @@ class TestNickname(BaseYang): + maxDiff = None + def setUp(self): BaseYang.setUp(self) @@ -50,8 +51,8 @@ def setUp(self): self._db_sess.commit() def test_to_enum_must_valid(self): - as_text = '//example-mod/CTRL/with-enum' - as_enum = '//65536/-3/4' + as_text = '//example/mod/CTRL/with-enum' + as_enum = '//65535/1/-3/4' ari_text = self._from_text(as_text) ari_enum = self._from_text(as_enum) @@ -61,7 +62,7 @@ def test_to_enum_must_valid(self): self.assertEqual(ari_enum, got_enum) def test_to_enum_must_nomod(self): - as_text = '//no-mod/CTRL/no-enum' + as_text = '//no/mod/CTRL/no-enum' ari_text = self._from_text(as_text) @@ -70,7 +71,7 @@ def test_to_enum_must_nomod(self): to_enum(ari_text) def test_to_enum_must_noobj(self): - as_text = '//example-mod/CTRL/missing' + as_text = '//example/mod/CTRL/missing' ari_text = self._from_text(as_text) @@ -79,8 +80,8 @@ def test_to_enum_must_noobj(self): to_enum(ari_text) def test_to_enum_nomust_valid(self): - as_text = '//example-mod/CTRL/with-enum' - as_enum = '//65536/-3/4' + as_text = '//example/mod/CTRL/with-enum' + as_enum = '//65535/1/-3/4' ari_text = self._from_text(as_text) ari_enum = self._from_text(as_enum) @@ -90,8 +91,8 @@ def test_to_enum_nomust_valid(self): self.assertEqual(ari_enum, got_enum) def test_to_enum_nomust_noobj(self): - as_text = '//example-mod/CTRL/missing' - as_enum = '//65536/-3/missing' + as_text = '//example/mod/CTRL/missing' + as_enum = '//65535/1/-3/missing' ari_text = self._from_text(as_text) ari_enum = self._from_text(as_enum) @@ -102,8 +103,8 @@ def test_to_enum_nomust_noobj(self): @unittest.expectedFailure def test_to_enum_nomust_noobjenum(self): - as_text = '//example-mod/CTRL/no-enum' - as_enum = '//65536/-3/no-enum' + as_text = '//example/mod/CTRL/no-enum' + as_enum = '//65535/1/-3/no-enum' ari_text = self._from_text(as_text) ari_enum = self._from_text(as_enum) @@ -113,8 +114,8 @@ def test_to_enum_nomust_noobjenum(self): self.assertEqual(ari_enum, got_enum) def test_from_enum_must_valid(self): - as_text = '//example-mod/CTRL/with-enum' - as_enum = '//65536/-3/4' + as_text = '//example/mod/CTRL/with-enum' + as_enum = '//65535/1/-3/4' ari_text = self._from_text(as_text) ari_enum = self._from_text(as_enum) @@ -124,7 +125,7 @@ def test_from_enum_must_valid(self): self.assertEqual(ari_text, got_text) def test_from_enum_must_nomod(self): - as_text = '//100/CTRL/4' + as_text = '//100/1/CTRL/4' ari_text = self._from_text(as_text) @@ -133,7 +134,7 @@ def test_from_enum_must_nomod(self): from_enum(ari_text) def test_from_enum_must_noobj(self): - as_text = '//65536/CTRL/100' + as_text = '//65535/1/CTRL/100' ari_text = self._from_text(as_text) @@ -142,8 +143,8 @@ def test_from_enum_must_noobj(self): from_enum(ari_text) def test_from_enum_nomust_valid(self): - as_text = '//example-mod/CTRL/with-enum' - as_enum = '//65536/-3/4' + as_text = '//example/mod/CTRL/with-enum' + as_enum = '//65535/1/-3/4' ari_text = self._from_text(as_text) ari_enum = self._from_text(as_enum) @@ -153,8 +154,8 @@ def test_from_enum_nomust_valid(self): self.assertEqual(ari_text, got_text) def test_from_enum_nomust_noobj(self): - as_text = '//example-mod/CTRL/missing' - as_enum = '//65536/-3/missing' + as_text = '//example/mod/CTRL/missing' + as_enum = '//65535/1/-3/missing' ari_text = self._from_text(as_text) ari_enum = self._from_text(as_enum) diff --git a/tests/test_typing.py b/tests/test_typing.py index 31e1531..353ef54 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -189,10 +189,10 @@ def test_edd_get(self): self.assertIsNone(typ.get(LiteralARI(b'hi'))) self.assertIsNone(typ.get(LiteralARI(123))) - ref = ReferenceARI(Identity(ns_id='mod', type_id=StructType.EDD, obj_id='name')) + ref = ReferenceARI(Identity(org_id='example', model_id='mod', type_id=StructType.EDD, obj_id='name')) self.assertEqual(ref, typ.get(ref)) - ref = ReferenceARI(Identity(ns_id='mod', type_id=StructType.CTRL, obj_id='name')) + ref = ReferenceARI(Identity(org_id='example', model_id='mod', type_id=StructType.CTRL, obj_id='name')) self.assertIsNone(typ.get(ref)) def test_edd_convert(self): @@ -208,10 +208,10 @@ def test_edd_convert(self): with self.assertRaises(TypeError): typ.convert(LiteralARI('hi')) - ref = ReferenceARI(Identity(ns_id='mod', type_id=StructType.EDD, obj_id='name')) + ref = ReferenceARI(Identity(org_id='example', model_id='mod', type_id=StructType.EDD, obj_id='name')) self.assertEqual(ref, typ.convert(ref)) - ref = ReferenceARI(Identity(ns_id='mod', type_id=StructType.CTRL, obj_id='name')) + ref = ReferenceARI(Identity(org_id='example', model_id='mod', type_id=StructType.CTRL, obj_id='name')) with self.assertRaises(ValueError): typ.convert(ref) From 33f6dafe5b6bec4b3c5445db84fd4a927bbefc46 Mon Sep 17 00:00:00 2001 From: Brian Sipos Date: Thu, 6 Feb 2025 15:53:09 -0500 Subject: [PATCH 3/3] Clarifying logic to split model-id from model-rev and normalizing internal representation --- pyproject.toml | 2 +- src/ace/ari.py | 4 +-- src/ace/ari_cbor.py | 44 ++++++++++++++++++++++++++------ src/ace/ari_text/encode.py | 2 +- src/ace/ari_text/parsemod.py | 6 ++--- src/ace/ari_text/util.py | 22 +++++++++++----- src/ace/lookup.py | 9 +++++-- src/ace/models.py | 10 +++++--- src/ace/nickname.py | 2 +- src/ace/pyang/dtnma_amm.py | 49 ++++++++++++++++++++++++++++-------- tests/test_ari_cbor.py | 2 ++ tests/test_ari_text.py | 3 +++ 12 files changed, 116 insertions(+), 39 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 09927d6..6c02458 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ ] dependencies = [ - "cbor2 ~=5.5", + "cbor2 ~=5.6.4", # specific patch version for tag 100 fix "cbor-diag", "numpy", "PLY >= 3", diff --git a/src/ace/ari.py b/src/ace/ari.py index 95599f6..66efa1d 100644 --- a/src/ace/ari.py +++ b/src/ace/ari.py @@ -284,7 +284,7 @@ class Identity: ''' The None value indicates an org-relative path. ''' model_id:Union[str, int, None] = None ''' The None value indicates an model-relative path. ''' - model_rev:Optional[str] = None + model_rev:Optional[datetime.date] = None ''' For the text-form ARI a specific ADM revision date. ''' type_id:Optional[StructType] = None ''' ADM type of the referenced object ''' @@ -294,7 +294,7 @@ class Identity: @property def ns_id(self) -> Tuple: ''' Get a tuple representing the namespace. ''' - return (self.org_id, self.model_id) + return (self.org_id, self.model_id, self.model_rev) @property def module_name(self) -> Optional[str]: diff --git a/src/ace/ari_cbor.py b/src/ace/ari_cbor.py index f89649e..bc8a6f5 100644 --- a/src/ace/ari_cbor.py +++ b/src/ace/ari_cbor.py @@ -24,7 +24,7 @@ ''' import datetime import logging -from typing import BinaryIO +from typing import BinaryIO, Optional import cbor2 from ace.ari import ( DTN_EPOCH, ARI, Identity, ReferenceARI, LiteralARI, StructType, @@ -68,22 +68,45 @@ def _item_to_ari(self, item:object): LOGGER.debug('Got ARI item: %s', item) if isinstance(item, list): - if len(item) in {4, 5}: + if len(item) in {4, 5, 6}: # Object reference - type_id = StructType(item[2]) if item[2] is not None else None + idx = 0 + org_id = item[idx] + idx += 1 + model_id = item[idx] + idx += 1 + + # cbor2 already handles date tags + if isinstance(item[idx], datetime.date): + model_rev = item[idx] + idx += 1 + else: + model_rev = None + + if item[idx] is not None: + type_id = StructType(item[idx]) + else: + type_id = None + idx += 1 + + obj_id = item[idx] + idx += 1 + ident = Identity( - org_id=item[0], - model_id=item[1], + org_id=org_id, + model_id=model_id, + model_rev=model_rev, type_id=type_id, - obj_id=item[3], + obj_id=obj_id, ) - params = None - if len(item) == 5: + if len(item) > idx: params = [ self._item_to_ari(param_item) for param_item in item[4] ] + else: + params = None res = ReferenceARI(ident=ident, params=params) @@ -186,6 +209,11 @@ def _ari_to_item(self, obj:ARI) -> object: item = [ obj.ident.org_id, obj.ident.model_id, + ] + if obj.ident.model_rev is not None: + # Be explicit about CBOR tag, use inverse of built-in conversion logic + item.append(cbor2.CBORTag(100, obj.ident.model_rev.toordinal() - 719163)) + item += [ type_id, obj.ident.obj_id, ] diff --git a/src/ace/ari_text/encode.py b/src/ace/ari_text/encode.py index 7f48e17..4090875 100644 --- a/src/ace/ari_text/encode.py +++ b/src/ace/ari_text/encode.py @@ -236,7 +236,7 @@ def _encode_obj(self, buf: TextIO, obj:ARI, prefix:bool=False): buf.write(str(obj.ident.model_id)) if obj.ident.model_rev is not None: buf.write('@') - buf.write(obj.ident.model_rev) + buf.write(obj.ident.model_rev.isoformat()) buf.write('/') if obj.ident.type_id is not None and obj.ident.obj_id is not None: diff --git a/src/ace/ari_text/parsemod.py b/src/ace/ari_text/parsemod.py index 8418b58..c665756 100644 --- a/src/ace/ari_text/parsemod.py +++ b/src/ace/ari_text/parsemod.py @@ -212,7 +212,7 @@ def p_objpath_only_ns(p): '''objpath : SLASH SLASH VALSEG SLASH VALSEG | SLASH SLASH VALSEG SLASH VALSEG SLASH''' org = util.IDSEGMENT(p[3]) - mod = util.MODID(p[5]) + mod = util.MODSEGMENT(p[5]) if not isinstance(mod, tuple): mod = (mod, None) @@ -228,7 +228,7 @@ def p_objpath_only_ns(p): def p_objpath_with_ns(p): 'objpath : SLASH SLASH VALSEG SLASH VALSEG SLASH VALSEG SLASH VALSEG' org = util.IDSEGMENT(p[3]) - mod = util.MODID(p[5]) + mod = util.MODSEGMENT(p[5]) if not isinstance(mod, tuple): mod = (mod, None) @@ -258,7 +258,7 @@ def p_objpath_relative(p): got = len(p) if got > 6: - mod = util.MODID(p[got - 5]) + mod = util.MODSEGMENT(p[got - 5]) if not isinstance(mod, tuple): mod = (mod, None) else: diff --git a/src/ace/ari_text/util.py b/src/ace/ari_text/util.py index 548ab19..174bf6c 100644 --- a/src/ace/ari_text/util.py +++ b/src/ace/ari_text/util.py @@ -124,14 +124,22 @@ def t_int(found): return int(found[0], 0) -@TypeMatch.apply(r'[a-zA-Z_][a-zA-Z0-9_\-\.]*') +@TypeMatch.apply(r'!?[a-zA-Z_][a-zA-Z0-9_\-\.]*') def t_identity(found): return found[0] -@TypeMatch.apply(r'(?P\!?[a-zA-Z_][a-zA-Z0-9_\-\.]*)(@(?P\d{4}-\d{2}-\d{2}))?') -def t_modid(found): - return (found['name'], found['rev']) +@TypeMatch.apply(r'(?P\!?[a-zA-Z_][a-zA-Z0-9_\-\.]*|[+-]?\d+)(@(?P\d{4}-\d{2}-\d{2}))?') +def t_modseg(found): + mod_id = found['name'] + if mod_id[0].isdigit() or mod_id[0] in {'+', '-'}: + mod_id = int(mod_id) + + mod_rev = found['rev'] + if mod_rev is not None: + mod_rev = datetime.date.fromisoformat(mod_rev) + + return (mod_id, mod_rev) def unescape(esc:str) -> str: @@ -167,10 +175,12 @@ def unescape(esc:str) -> str: txt += char return txt + def decode_unicode(hex_str): code_point = int(hex_str, 16) return chr(code_point) + @TypeMatch.apply(r'"(?P(?:[^"]|\\.)*)"') def t_tstr(found): return unescape(found['val']) @@ -261,8 +271,8 @@ def t_timeperiod(found): IDSEGMENT = TypeSeq([t_int, t_identity]) ''' Either an integer or identity text. ''' -MODID = TypeSeq([t_int, t_modid]) -''' Module namespace identity. ''' +MODSEGMENT = TypeSeq([t_modseg]) +''' Model namespace segment as a tuple of ID and revision. ''' SINGLETONS = TypeSeq([ t_undefined, diff --git a/src/ace/lookup.py b/src/ace/lookup.py index 8985cf4..7440535 100644 --- a/src/ace/lookup.py +++ b/src/ace/lookup.py @@ -25,6 +25,7 @@ import copy from dataclasses import dataclass +import datetime import logging from typing import Dict, List, Optional, Union from sqlalchemy.orm.session import Session, object_session @@ -74,7 +75,8 @@ def __call__(self, ari:ARI) -> ARI: return ari -def find_adm(org_id: Union[str, int], model_id: Union[str, int], db_sess:Session) -> Optional[AdmModule]: +def find_adm(org_id: Union[str, int], model_id: Union[str, int], + model_rev: Optional[datetime.date], db_sess:Session) -> Optional[AdmModule]: ''' Dereference an ADM module. ''' query_adm = db_sess.query(AdmModule) @@ -93,6 +95,9 @@ def find_adm(org_id: Union[str, int], model_id: Union[str, int], db_sess:Session else: raise TypeError(f'ReferenceARI model_id is not int or str: {model_id}') + if model_rev is not None: + query_adm = query_adm.filter(AdmModule.latest_revision_date == model_rev) + found_adm = query_adm.one_or_none() return found_adm @@ -102,7 +107,7 @@ def dereference(ref:ReferenceARI, db_sess:Session) -> Optional[AdmObjMixin]: ''' orm_type = ORM_TYPE[ref.ident.type_id] - found_adm = find_adm(ref.ident.org_id, ref.ident.model_id, db_sess) + found_adm = find_adm(ref.ident.org_id, ref.ident.model_id, ref.ident.model_rev, db_sess) if found_adm is None: return None diff --git a/src/ace/models.py b/src/ace/models.py index f16b0ec..9e640c3 100644 --- a/src/ace/models.py +++ b/src/ace/models.py @@ -23,7 +23,7 @@ ''' ORM models for the ADM and its contents. ''' from sqlalchemy import ( - Column, ForeignKey, Integer, String, DateTime, Text, PickleType + Column, ForeignKey, Integer, String, Date, DateTime, Text, PickleType ) from sqlalchemy.orm import ( declarative_base, relationship, declared_attr, Mapped @@ -31,7 +31,7 @@ from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.orderinglist import ordering_list -CURRENT_SCHEMA_VERSION = 18 +CURRENT_SCHEMA_VERSION = 19 ''' Value of :attr:`SchemaVersion.version_num` ''' Base = declarative_base() @@ -200,8 +200,8 @@ class AdmModule(Base): ) @hybrid_property - def latest_revision(self): - return max(rev.name for rev in self.revisions) + def latest_revision_date(self): + return max(rev.date for rev in self.revisions) imports = relationship( "AdmImport", @@ -265,6 +265,8 @@ class AdmRevision(Base, CommonMixin): # Original exact text, indexed for sorting name = Column(String, index=True) + # Parsed date + date = Column(Date, index=True) class AdmImport(Base, CommonMixin): diff --git a/src/ace/nickname.py b/src/ace/nickname.py index 333342a..8bcdeba 100644 --- a/src/ace/nickname.py +++ b/src/ace/nickname.py @@ -71,7 +71,7 @@ def _convert_ref(self, ari:ReferenceARI) -> ReferenceARI: if obj: adm = obj.module else: - adm = find_adm(ari.ident.org_id, ari.ident.model_id, self._db_sess) + adm = find_adm(ari.ident.org_id, ari.ident.model_id, ari.ident.model_rev, self._db_sess) LOGGER.debug('ARI for %s resolved to ADM %s, obj %s', ari.ident, adm, obj) diff --git a/src/ace/pyang/dtnma_amm.py b/src/ace/pyang/dtnma_amm.py index 597245b..b4fb318 100644 --- a/src/ace/pyang/dtnma_amm.py +++ b/src/ace/pyang/dtnma_amm.py @@ -31,6 +31,7 @@ import io from typing import List, Tuple from pyang import plugin, context, statements, syntax, grammar, error +from pyang.util import keyword_to_str # Use ARI processing library when possible try: @@ -130,40 +131,58 @@ def pyang_plugin_init(): ['module'], _stmt_check_enum_unique ) + statements.add_validation_fun( + 'grammar', + AMM_OBJ_NAMES + + ( + (MODULE_NAME, 'parameter'), + (MODULE_NAME, 'operand'), + (MODULE_NAME, 'result'), + ), + _stmt_check_documentation + ) # Register special error codes error.add_error_code( - 'AMM_MODULE_NS', 1, + 'AMM_MODULE_NS', 1, # critical "An ADM module must have an ARI namespace, not %s" ) error.add_error_code( - 'AMM_MODULE_OBJS', 1, + 'AMM_MODULE_OBJS', 1, # critical "An ADM module cannot contain a statement %r named \"%s\"" ) error.add_error_code( - 'AMM_MODULE_ENUM', 4, - "The ADM module %s must contain an amm:enum statement" + 'AMM_MODULE_ENUM', 4, # warning + "The ADM module \"%s\" must contain an amm:enum statement" ) error.add_error_code( - 'AMM_OBJ_ENUM', 4, + 'AMM_OBJ_ENUM', 4, # warning "The ADM object %s named \"%s\" should contain an amm:enum statement" ) error.add_error_code( - 'AMM_OBJ_ENUM_UNIQUE', 1, + 'AMM_OBJ_ENUM_UNIQUE', 1, # critical "An amm:enum must be unique among all %s objects, has value %s" ) error.add_error_code( - 'AMM_INTLABELS', 1, - "An amm:int-labels must have either 'enum' or 'bit' statements %s" + 'AMM_INTLABELS', 1, # critical + "An amm:int-labels must have either 'enum' or 'bit' substatements in \"%s\"" ) error.add_error_code( - 'AMM_INTLABELS_ENUM_VALUE', 1, + 'AMM_INTLABELS_ENUM_VALUE', 1, # critical "An amm:int-labels 'enum' statement %r must have a unique 'value'" ) error.add_error_code( - 'AMM_INTLABELS_BIT_VALUE', 1, + 'AMM_INTLABELS_BIT_VALUE', 1, # critical "An amm:int-labels 'bit' statement %r must have a unique 'position'" ) + error.add_error_code( + 'AMM_DOC_DESCRIPTION', 4, # warning + "A description should be present under %s statement \"%s\"" + ) + error.add_error_code( + 'AMM_DOC_REFERENCE', 4, # warning + "A reference should be present under %s statement \"%s\"" + ) @dataclass @@ -596,7 +615,7 @@ def _stmt_check_module_objs(ctx:context.Context, stmt:statements.Statement): for sub in stmt.substmts: if sub.keyword not in allowed: error.err_add(ctx.errors, sub.pos, 'AMM_MODULE_OBJS', - (sub.keyword, sub.arg)) + (keyword_to_str(sub.keyword), sub.arg)) def _stmt_check_obj_enum(ctx:context.Context, stmt:statements.Statement): @@ -657,3 +676,11 @@ def _stmt_check_enum_unique(ctx:context.Context, stmt:statements.Statement): error.err_add(ctx.errors, obj_stmt.pos, 'AMM_OBJ_ENUM_UNIQUE', (obj_kywd[1], enum_stmt.arg)) seen_enum.add(enum_val) + +def _stmt_check_documentation(ctx:context.Context, stmt:statements.Statement): + if stmt.search_one('description') is None: + error.err_add(ctx.errors, stmt.pos, 'AMM_DOC_DESCRIPTION', + (keyword_to_str(stmt.keyword), stmt.arg)) + if stmt.search_one('reference') is None and False: + error.err_add(ctx.errors, stmt.pos, 'AMM_DOC_REFERENCE', + (keyword_to_str(stmt.keyword), stmt.arg)) diff --git a/tests/test_ari_cbor.py b/tests/test_ari_cbor.py index c84d267..a2705bb 100644 --- a/tests/test_ari_cbor.py +++ b/tests/test_ari_cbor.py @@ -93,6 +93,8 @@ def test_literal_cbor_loopback(self): cbor2.dumps([65535, 1, None, None]), # from `ari://65535/hi/` cbor2.dumps([65535, "hi", None, None]), + # from 'ari:/ietf/dtnma-agent@2024-06-25/', + cbor2.dumps([1, 1, cbor2.CBORTag(100, 19899), None, None]), # from `ari://65535/1/CTRL/0` cbor2.dumps([65535, 1, StructType.CTRL.value, 0]), # from 'ari:/ietf/bp-agent/CTRL/reset_all_counts()', diff --git a/tests/test_ari_text.py b/tests/test_ari_text.py index bfef109..d386fde 100644 --- a/tests/test_ari_text.py +++ b/tests/test_ari_text.py @@ -279,6 +279,9 @@ def test_literal_text_options(self): 'ari://example/namespace/VAR/hello(//example/other/CONST/hi)', 'ari://example/namespace@2020-01-01/VAR/hello', 'ari://65535/0/CTRL/0', + 'ari://!private/adm/', + 'ari://!private/adm@2024-02-06/', + 'ari://!private/!odm/', 'ari:./VAR/hello', 'ari://ietf/bp-agent/CTRL/reset_all_counts()', 'ari://ietf/amp-agent/CTRL/gen_rpts(/AC/(//ietf/bpsec/CONST/source_report(%22ipn%3A1.1%22)),/AC/())',