120 lines
3.8 KiB
Python
120 lines
3.8 KiB
Python
import urllib.parse
|
|
from collections.abc import Iterator
|
|
from typing import Any
|
|
|
|
STRICT_VALIDATION = True
|
|
|
|
Key = int | str | urllib.parse.ParseResult
|
|
Context = urllib.parse.ParseResult # TODO
|
|
|
|
|
|
def canonical_keys(base_key: Key, context: Context | None) -> list[Any]:
|
|
if isinstance(base_key, urllib.parse.ParseResult):
|
|
return [base_key]
|
|
if not isinstance(base_key, str):
|
|
return [base_key]
|
|
if base_key.startswith('@'):
|
|
return [base_key]
|
|
if context is None:
|
|
return [base_key]
|
|
return [context._replace(path=base_key), base_key]
|
|
|
|
|
|
class Concept:
|
|
def __init__(self, context: Context | None, pairs: dict[Key, str]) -> None:
|
|
self.pairs = []
|
|
for k, v in pairs.items():
|
|
keys = canonical_keys(k, context)
|
|
self.pairs.append(
|
|
{'canonical_key': keys[0], 'keys': set(keys), 'values': v},
|
|
)
|
|
self.regenerate_by_keys()
|
|
|
|
def regenerate_by_keys(self) -> None:
|
|
self.by_keys = {k: pair for pair in self.pairs for k in pair['keys']}
|
|
|
|
def __copy__(self) -> 'Concept':
|
|
new = Concept(None, {})
|
|
for p in self.pairs:
|
|
new.pairs.append(
|
|
{
|
|
'canonical_key': p['canonical_key'],
|
|
'keys': set(p['keys']),
|
|
'values': p['values'],
|
|
},
|
|
)
|
|
new.regenerate_by_keys()
|
|
return new
|
|
|
|
def get(self, key: Key, default=None):
|
|
pairs = self.by_keys.get(key, None)
|
|
return pairs['values'] if pairs is not None else default
|
|
|
|
def getlist(self, key: Key) -> list[Any]:
|
|
result = self.get(key)
|
|
if result is None:
|
|
return []
|
|
if not isinstance(result, list):
|
|
msg = f'Not a list: {result}'
|
|
raise TypeError(msg)
|
|
return [r['value'] for r in result]
|
|
|
|
def keys(self) -> Iterator[Key]:
|
|
for pair in self.pairs:
|
|
yield pair['canonical_key']
|
|
|
|
def setdefault(self, key: Key, value):
|
|
if key not in self.by_keys:
|
|
self[key] = value
|
|
return self.by_keys[key]['values']
|
|
|
|
def to_dict(self) -> dict[Key, Any]:
|
|
return {p['canonical_key']: p['values'] for p in self.pairs}
|
|
|
|
def __getitem__(self, key: Key):
|
|
return self.by_keys[key]['values']
|
|
|
|
def __setitem__(self, key: Key, value) -> None:
|
|
if STRICT_VALIDATION:
|
|
if not isinstance(key, str) or key != '@id':
|
|
if not isinstance(value, list):
|
|
raise TypeError(value)
|
|
for v in value:
|
|
if not isinstance(value, dict):
|
|
raise TypeError(value)
|
|
if 'value' not in v:
|
|
raise TypeError(value)
|
|
for subk in v:
|
|
if isinstance(v[subk], list):
|
|
raise TypeError(value)
|
|
del subk
|
|
del v
|
|
|
|
if key in self.by_keys:
|
|
self.by_keys[key]['values'] = value
|
|
else:
|
|
pair = {'canonical_key': key, 'keys': {key}, 'values': value}
|
|
self.pairs.append(pair)
|
|
self.by_keys[key] = pair
|
|
|
|
def __contains__(self, key: Key) -> bool:
|
|
return key in self.by_keys
|
|
|
|
def __delitem__(self, key: Key) -> None:
|
|
self.pairs.remove(self.by_keys[key])
|
|
del self.by_keys[key]
|
|
|
|
def __repr__(self) -> str:
|
|
if object_id := self.by_keys.get('@id'):
|
|
return 'Concept {{ @id = {} }}'.format(object_id['values'])
|
|
|
|
return 'Concept ' + str({p['canonical_key']: p['values'] for p in self.pairs})
|
|
|
|
def __str__(self) -> str:
|
|
return repr(self)
|
|
|
|
def set_canonical_key(self, new_canonical_key: Key, key: Key | None = None):
|
|
if key is None:
|
|
key = new_canonical_key
|
|
self.by_keys[key]['canonical_key'] = new_canonical_key
|