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,118 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module is intended to replicate some of the functionality
|
||||
from the fastnumbers module in the event that module is not installed.
|
||||
"""
|
||||
import unicodedata
|
||||
from typing import Callable, FrozenSet, Union
|
||||
|
||||
from natsort.unicode_numbers import decimal_chars
|
||||
|
||||
_NAN_INF = [
|
||||
"INF",
|
||||
"INf",
|
||||
"Inf",
|
||||
"inF",
|
||||
"iNF",
|
||||
"InF",
|
||||
"inf",
|
||||
"iNf",
|
||||
"NAN",
|
||||
"nan",
|
||||
"NaN",
|
||||
"nAn",
|
||||
"naN",
|
||||
"NAn",
|
||||
"nAN",
|
||||
"Nan",
|
||||
]
|
||||
_NAN_INF.extend(["+" + x[:2] for x in _NAN_INF] + ["-" + x[:2] for x in _NAN_INF])
|
||||
NAN_INF = frozenset(_NAN_INF)
|
||||
ASCII_NUMS = "0123456789+-"
|
||||
POTENTIAL_FIRST_CHAR = frozenset(decimal_chars + list(ASCII_NUMS + "."))
|
||||
|
||||
StrOrFloat = Union[str, float]
|
||||
StrOrInt = Union[str, int]
|
||||
|
||||
|
||||
def fast_float(
|
||||
x: str,
|
||||
key: Callable[[str], str] = lambda x: x,
|
||||
nan: float = float("inf"),
|
||||
_uni: Callable[[str, StrOrFloat], StrOrFloat] = unicodedata.numeric,
|
||||
_nan_inf: FrozenSet[str] = NAN_INF,
|
||||
_first_char: FrozenSet[str] = POTENTIAL_FIRST_CHAR,
|
||||
) -> StrOrFloat:
|
||||
"""
|
||||
Convert a string to a float quickly, return input as-is if not possible.
|
||||
|
||||
We don't need to accept all input that the real fast_int accepts because
|
||||
natsort is controlling what is passed to this function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : str
|
||||
String to attempt to convert to a float.
|
||||
key : callable
|
||||
Single-argument function to apply to *x* if conversion fails.
|
||||
nan : float
|
||||
Value to return instead of NaN if NaN would be returned.
|
||||
|
||||
Returns
|
||||
-------
|
||||
*str* or *float*
|
||||
|
||||
"""
|
||||
if x[0] in _first_char or x.lstrip()[:3] in _nan_inf:
|
||||
try:
|
||||
ret = float(x)
|
||||
return nan if ret != ret else ret
|
||||
except ValueError:
|
||||
try:
|
||||
return _uni(x, key(x)) if len(x) == 1 else key(x)
|
||||
except TypeError: # pragma: no cover
|
||||
return key(x)
|
||||
else:
|
||||
try:
|
||||
return _uni(x, key(x)) if len(x) == 1 else key(x)
|
||||
except TypeError: # pragma: no cover
|
||||
return key(x)
|
||||
|
||||
|
||||
def fast_int(
|
||||
x: str,
|
||||
key: Callable[[str], str] = lambda x: x,
|
||||
_uni: Callable[[str, StrOrInt], StrOrInt] = unicodedata.digit,
|
||||
_first_char: FrozenSet[str] = POTENTIAL_FIRST_CHAR,
|
||||
) -> StrOrInt:
|
||||
"""
|
||||
Convert a string to a int quickly, return input as-is if not possible.
|
||||
|
||||
We don't need to accept all input that the real fast_int accepts because
|
||||
natsort is controlling what is passed to this function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : str
|
||||
String to attempt to convert to an int.
|
||||
key : callable
|
||||
Single-argument function to apply to *x* if conversion fails.
|
||||
|
||||
Returns
|
||||
-------
|
||||
*str* or *int*
|
||||
|
||||
"""
|
||||
if x[0] in _first_char:
|
||||
try:
|
||||
return int(x)
|
||||
except ValueError:
|
||||
try:
|
||||
return _uni(x, key(x)) if len(x) == 1 else key(x)
|
||||
except TypeError: # pragma: no cover
|
||||
return key(x)
|
||||
else:
|
||||
try:
|
||||
return _uni(x, key(x)) if len(x) == 1 else key(x)
|
||||
except TypeError: # pragma: no cover
|
||||
return key(x)
|
||||
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Interface for natsort to access fastnumbers functions without
|
||||
having to worry if it is actually installed.
|
||||
"""
|
||||
import re
|
||||
from typing import Callable, Iterable, Iterator, Tuple, Union
|
||||
|
||||
StrOrFloat = Union[str, float]
|
||||
StrOrInt = Union[str, int]
|
||||
|
||||
__all__ = ["try_float", "try_int"]
|
||||
|
||||
|
||||
def is_supported_fastnumbers(
|
||||
fastnumbers_version: str, minimum: Tuple[int, int, int] = (2, 0, 0)
|
||||
) -> bool:
|
||||
match = re.match(
|
||||
r"^(\d+)\.(\d+)(\.(\d+))?([ab](\d+))?$",
|
||||
fastnumbers_version,
|
||||
flags=re.ASCII,
|
||||
)
|
||||
|
||||
if not match:
|
||||
raise ValueError(
|
||||
"Invalid fastnumbers version number '{}'".format(fastnumbers_version)
|
||||
)
|
||||
|
||||
(major, minor, patch) = match.group(1, 2, 4)
|
||||
|
||||
return (int(major), int(minor), int(patch)) >= minimum
|
||||
|
||||
|
||||
# If the user has fastnumbers installed, they will get great speed
|
||||
# benefits. If not, we use the simulated functions that come with natsort.
|
||||
try:
|
||||
# noinspection PyPackageRequirements
|
||||
from fastnumbers import fast_float, fast_int, __version__ as fn_ver
|
||||
|
||||
# Require >= version 2.0.0.
|
||||
if not is_supported_fastnumbers(fn_ver):
|
||||
raise ImportError # pragma: no cover
|
||||
|
||||
# For versions of fastnumbers with mapping capability, use that
|
||||
if is_supported_fastnumbers(fn_ver, (5, 0, 0)):
|
||||
del fast_float, fast_int
|
||||
from fastnumbers import try_float, try_int
|
||||
except ImportError:
|
||||
from natsort.compat.fake_fastnumbers import fast_float, fast_int # type: ignore
|
||||
|
||||
# Re-map the old-or-compatibility functions fast_float/fast_int to the
|
||||
# newer API of try_float/try_int. If we already imported try_float/try_int
|
||||
# then there is nothing to do.
|
||||
if "try_float" not in globals():
|
||||
|
||||
def try_float( # type: ignore[no-redef] # noqa: F811
|
||||
x: Iterable[str],
|
||||
map: bool,
|
||||
nan: float = float("inf"),
|
||||
on_fail: Callable[[str], str] = lambda x: x,
|
||||
) -> Iterator[StrOrFloat]:
|
||||
assert map is True
|
||||
return (fast_float(y, nan=nan, key=on_fail) for y in x)
|
||||
|
||||
|
||||
if "try_int" not in globals():
|
||||
|
||||
def try_int( # type: ignore[no-redef] # noqa: F811
|
||||
x: Iterable[str],
|
||||
map: bool,
|
||||
on_fail: Callable[[str], str] = lambda x: x,
|
||||
) -> Iterator[StrOrInt]:
|
||||
assert map is True
|
||||
return (fast_int(y, key=on_fail) for y in x)
|
||||
@@ -0,0 +1,122 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Interface for natsort to access locale functionality without
|
||||
having to worry about if it is using PyICU or the built-in locale.
|
||||
"""
|
||||
import sys
|
||||
from typing import Callable, Union, cast
|
||||
|
||||
StrOrBytes = Union[str, bytes]
|
||||
TrxfmFunc = Callable[[str], StrOrBytes]
|
||||
|
||||
# This string should be sorted after any other byte string because
|
||||
# it contains the max unicode character repeated 20 times.
|
||||
# You would need some odd data to come after that.
|
||||
null_string = ""
|
||||
null_string_max = chr(sys.maxunicode) * 20
|
||||
|
||||
# This variable could be str or bytes depending on the locale library
|
||||
# being used, so give the type-checker this information.
|
||||
null_string_locale: StrOrBytes
|
||||
null_string_locale_max: StrOrBytes
|
||||
|
||||
# strxfrm can be buggy (especially on OSX and *possibly* some other
|
||||
# BSD-based systems), so prefer icu if available.
|
||||
try: # noqa: C901
|
||||
import icu
|
||||
from locale import getlocale
|
||||
|
||||
null_string_locale = b""
|
||||
|
||||
# This string should in theory be sorted after any other byte
|
||||
# string because it contains the max byte char repeated many times.
|
||||
# You would need some odd data to come after that.
|
||||
null_string_locale_max = b"x7f" * 50
|
||||
|
||||
def dumb_sort() -> bool:
|
||||
return False
|
||||
|
||||
# If using icu, get the locale from the current global locale,
|
||||
def get_icu_locale() -> str:
|
||||
language_code, encoding = getlocale()
|
||||
if language_code is None or encoding is None: # pragma: no cover
|
||||
return icu.Locale()
|
||||
return icu.Locale(f"{language_code}.{encoding}")
|
||||
|
||||
def get_strxfrm() -> TrxfmFunc:
|
||||
return icu.Collator.createInstance(get_icu_locale()).getSortKey
|
||||
|
||||
def get_thousands_sep() -> str:
|
||||
sep = icu.DecimalFormatSymbols.kGroupingSeparatorSymbol
|
||||
return icu.DecimalFormatSymbols(get_icu_locale()).getSymbol(sep)
|
||||
|
||||
def get_decimal_point() -> str:
|
||||
sep = icu.DecimalFormatSymbols.kDecimalSeparatorSymbol
|
||||
return icu.DecimalFormatSymbols(get_icu_locale()).getSymbol(sep)
|
||||
|
||||
except ImportError:
|
||||
import locale
|
||||
from locale import strxfrm
|
||||
|
||||
null_string_locale = null_string
|
||||
null_string_locale_max = null_string_max
|
||||
|
||||
# On some systems, locale is broken and does not sort in the expected
|
||||
# order. We will try to detect this and compensate.
|
||||
def dumb_sort() -> bool:
|
||||
return strxfrm("A") < strxfrm("a")
|
||||
|
||||
def get_strxfrm() -> TrxfmFunc:
|
||||
return strxfrm
|
||||
|
||||
def get_thousands_sep() -> str:
|
||||
sep = cast(str, locale.localeconv()["thousands_sep"])
|
||||
# If this locale library is broken, some of the thousands separator
|
||||
# characters are incorrectly blank. Here is a lookup table of the
|
||||
# corrections I am aware of.
|
||||
if dumb_sort():
|
||||
language_code, encoding = locale.getlocale()
|
||||
if language_code is None or encoding is None:
|
||||
# No locale loaded, default to ','
|
||||
return ","
|
||||
loc = f"{language_code}.{encoding}"
|
||||
return {
|
||||
"de_DE.ISO8859-15": ".",
|
||||
"es_ES.ISO8859-1": ".",
|
||||
"de_AT.ISO8859-1": ".",
|
||||
"de_at": "\xa0",
|
||||
"nl_NL.UTF-8": ".",
|
||||
"es_es": ".",
|
||||
"fr_CH.ISO8859-15": "\xa0",
|
||||
"fr_CA.ISO8859-1": "\xa0",
|
||||
"de_CH.ISO8859-1": ".",
|
||||
"fr_FR.ISO8859-15": "\xa0",
|
||||
"nl_NL.ISO8859-1": ".",
|
||||
"ca_ES.UTF-8": ".",
|
||||
"nl_NL.ISO8859-15": ".",
|
||||
"de_ch": "'",
|
||||
"ca_es": ".",
|
||||
"de_AT.ISO8859-15": ".",
|
||||
"ca_ES.ISO8859-1": ".",
|
||||
"de_AT.UTF-8": ".",
|
||||
"es_ES.UTF-8": ".",
|
||||
"fr_fr": "\xa0",
|
||||
"es_ES.ISO8859-15": ".",
|
||||
"de_DE.ISO8859-1": ".",
|
||||
"nl_nl": ".",
|
||||
"fr_ch": "\xa0",
|
||||
"fr_ca": "\xa0",
|
||||
"de_DE.UTF-8": ".",
|
||||
"ca_ES.ISO8859-15": ".",
|
||||
"de_CH.ISO8859-15": ".",
|
||||
"fr_FR.ISO8859-1": "\xa0",
|
||||
"fr_CH.ISO8859-1": "\xa0",
|
||||
"de_de": ".",
|
||||
"fr_FR.UTF-8": "\xa0",
|
||||
"fr_CA.ISO8859-15": "\xa0",
|
||||
}.get(loc, sep)
|
||||
else:
|
||||
return sep
|
||||
|
||||
def get_decimal_point() -> str:
|
||||
return cast(str, locale.localeconv()["decimal_point"])
|
||||
Reference in New Issue
Block a user