feat: referentiel projets dynamique + validation nommage by design
Durcit la convention de nommage des projets (dérive constatée : 'Sliding Automation', 'code_versioning'... au lieu des formes canoniques). - trilium_api.py : projets_canoniques() lit le référentiel = valeurs du label projet sur les notes de type=projet (source unique, pas de constante en dur). Note-projet CodeVersioning créée (manquait). - mcp_server.py : _valider_projet() branché dans les 6 tools de création (add_decision/history/backlog, new_conversation, create_entite, add_skill). Refuse un projet non canonique (suggestion si faute) ou inconnu (renvoi au processus de création de projet). Ne verrouille pas si référentiel illisible. - lint_audit.py : VAL-nommage aligné sur le référentiel (attrape casse, espace ET snake_case ; l'ancien 'contient un espace' ratait code_versioning). - Données : 79 notes ré-étiquetées vers les 3 formes canoniques. Quality by design : l'erreur de nommage devient impossible à l'écriture, le Lint n'est plus que le filet de sécurité.
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import email.message
|
||||
import email.policy
|
||||
import re
|
||||
import textwrap
|
||||
|
||||
from ._text import FoldedCase
|
||||
|
||||
|
||||
class RawPolicy(email.policy.EmailPolicy):
|
||||
def fold(self, name, value):
|
||||
folded = self.linesep.join(
|
||||
textwrap
|
||||
.indent(value, prefix=' ' * 8, predicate=lambda line: True)
|
||||
.lstrip()
|
||||
.splitlines()
|
||||
)
|
||||
return f'{name}: {folded}{self.linesep}'
|
||||
|
||||
|
||||
class Message(email.message.Message):
|
||||
r"""
|
||||
Specialized Message subclass to handle metadata naturally.
|
||||
|
||||
Reads values that may have newlines in them and converts the
|
||||
payload to the Description.
|
||||
|
||||
>>> msg_text = textwrap.dedent('''
|
||||
... Name: Foo
|
||||
... Version: 3.0
|
||||
... License: blah
|
||||
... de-blah
|
||||
... <BLANKLINE>
|
||||
... First line of description.
|
||||
... Second line of description.
|
||||
... <BLANKLINE>
|
||||
... Fourth line!
|
||||
... ''').lstrip().replace('<BLANKLINE>', '')
|
||||
>>> msg = Message(email.message_from_string(msg_text))
|
||||
>>> msg['Description']
|
||||
'First line of description.\nSecond line of description.\n\nFourth line!\n'
|
||||
|
||||
Message should render even if values contain newlines.
|
||||
|
||||
>>> print(msg)
|
||||
Name: Foo
|
||||
Version: 3.0
|
||||
License: blah
|
||||
de-blah
|
||||
Description: First line of description.
|
||||
Second line of description.
|
||||
<BLANKLINE>
|
||||
Fourth line!
|
||||
<BLANKLINE>
|
||||
<BLANKLINE>
|
||||
"""
|
||||
|
||||
multiple_use_keys = set(
|
||||
map(
|
||||
FoldedCase,
|
||||
[
|
||||
'Classifier',
|
||||
'Obsoletes-Dist',
|
||||
'Platform',
|
||||
'Project-URL',
|
||||
'Provides-Dist',
|
||||
'Provides-Extra',
|
||||
'Requires-Dist',
|
||||
'Requires-External',
|
||||
'Supported-Platform',
|
||||
'Dynamic',
|
||||
],
|
||||
)
|
||||
)
|
||||
"""
|
||||
Keys that may be indicated multiple times per PEP 566.
|
||||
"""
|
||||
|
||||
def __new__(cls, orig: email.message.Message):
|
||||
res = super().__new__(cls)
|
||||
vars(res).update(vars(orig))
|
||||
return res
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._headers = self._repair_headers()
|
||||
|
||||
# suppress spurious error from mypy
|
||||
def __iter__(self):
|
||||
return super().__iter__()
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""
|
||||
Override parent behavior to typical dict behavior.
|
||||
|
||||
``email.message.Message`` will emit None values for missing
|
||||
keys. Typical mappings, including this ``Message``, will raise
|
||||
a key error for missing keys.
|
||||
|
||||
Ref python/importlib_metadata#371.
|
||||
"""
|
||||
res = super().__getitem__(item)
|
||||
if res is None:
|
||||
raise KeyError(item)
|
||||
return res
|
||||
|
||||
def _repair_headers(self):
|
||||
def redent(value):
|
||||
"Correct for RFC822 indentation"
|
||||
indent = ' ' * 8
|
||||
if not value or '\n' + indent not in value:
|
||||
return value
|
||||
return textwrap.dedent(indent + value)
|
||||
|
||||
headers = [(key, redent(value)) for key, value in vars(self)['_headers']]
|
||||
if self._payload:
|
||||
headers.append(('Description', self.get_payload()))
|
||||
self.set_payload('')
|
||||
return headers
|
||||
|
||||
def as_string(self):
|
||||
return super().as_string(policy=RawPolicy())
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
"""
|
||||
Convert PackageMetadata to a JSON-compatible format
|
||||
per PEP 0566.
|
||||
"""
|
||||
|
||||
def transform(key):
|
||||
value = self.get_all(key) if key in self.multiple_use_keys else self[key]
|
||||
if key == 'Keywords':
|
||||
value = re.split(r'\s+', value)
|
||||
tk = key.lower().replace('-', '_')
|
||||
return tk, value
|
||||
|
||||
return dict(map(transform, map(FoldedCase, self)))
|
||||
Reference in New Issue
Block a user