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,185 @@
|
||||
import datetime
|
||||
import re
|
||||
|
||||
|
||||
class Frequencies:
|
||||
@staticmethod
|
||||
def hourly(t):
|
||||
dt = t + datetime.timedelta(hours=1)
|
||||
return dt.replace(minute=0, second=0, microsecond=0)
|
||||
|
||||
@staticmethod
|
||||
def daily(t):
|
||||
dt = t + datetime.timedelta(days=1)
|
||||
return dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
@staticmethod
|
||||
def weekly(t):
|
||||
dt = t + datetime.timedelta(days=7 - t.weekday())
|
||||
return dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
@staticmethod
|
||||
def monthly(t):
|
||||
if t.month == 12:
|
||||
y, m = t.year + 1, 1
|
||||
else:
|
||||
y, m = t.year, t.month + 1
|
||||
return t.replace(year=y, month=m, day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
@staticmethod
|
||||
def yearly(t):
|
||||
y = t.year + 1
|
||||
return t.replace(year=y, month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
|
||||
def parse_size(size):
|
||||
size = size.strip()
|
||||
reg = re.compile(r"([e\+\-\.\d]+)\s*([kmgtpezy])?(i)?(b)", flags=re.I)
|
||||
|
||||
match = reg.fullmatch(size)
|
||||
|
||||
if not match:
|
||||
return None
|
||||
|
||||
s, u, i, b = match.groups()
|
||||
|
||||
try:
|
||||
s = float(s)
|
||||
except ValueError as e:
|
||||
raise ValueError("Invalid float value while parsing size: '%s'" % s) from e
|
||||
|
||||
u = "kmgtpezy".index(u.lower()) + 1 if u else 0
|
||||
i = 1024 if i else 1000
|
||||
b = {"b": 8, "B": 1}[b] if b else 1
|
||||
return s * i**u / b
|
||||
|
||||
|
||||
def parse_duration(duration):
|
||||
duration = duration.strip()
|
||||
reg = r"(?:([e\+\-\.\d]+)\s*([a-z]+)[\s\,]*)"
|
||||
|
||||
units = [
|
||||
("y|years?", 31536000),
|
||||
("months?", 2628000),
|
||||
("w|weeks?", 604800),
|
||||
("d|days?", 86400),
|
||||
("h|hours?", 3600),
|
||||
("min(?:ute)?s?", 60),
|
||||
("s|sec(?:ond)?s?", 1), # spellchecker: disable-line
|
||||
("ms|milliseconds?", 0.001),
|
||||
("us|microseconds?", 0.000001),
|
||||
]
|
||||
|
||||
if not re.fullmatch(reg + "+", duration, flags=re.I):
|
||||
return None
|
||||
|
||||
seconds = 0
|
||||
|
||||
for value, unit in re.findall(reg, duration, flags=re.I):
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError as e:
|
||||
raise ValueError("Invalid float value while parsing duration: '%s'" % value) from e
|
||||
|
||||
try:
|
||||
unit = next(u for r, u in units if re.fullmatch(r, unit, flags=re.I))
|
||||
except StopIteration:
|
||||
raise ValueError("Invalid unit value while parsing duration: '%s'" % unit) from None
|
||||
|
||||
seconds += value * unit
|
||||
|
||||
return datetime.timedelta(seconds=seconds)
|
||||
|
||||
|
||||
def parse_frequency(frequency):
|
||||
frequencies = {
|
||||
"hourly": Frequencies.hourly,
|
||||
"daily": Frequencies.daily,
|
||||
"weekly": Frequencies.weekly,
|
||||
"monthly": Frequencies.monthly,
|
||||
"yearly": Frequencies.yearly,
|
||||
}
|
||||
frequency = frequency.strip().lower()
|
||||
return frequencies.get(frequency, None)
|
||||
|
||||
|
||||
def parse_day(day):
|
||||
days = {
|
||||
"monday": 0,
|
||||
"tuesday": 1,
|
||||
"wednesday": 2,
|
||||
"thursday": 3,
|
||||
"friday": 4,
|
||||
"saturday": 5,
|
||||
"sunday": 6,
|
||||
}
|
||||
day = day.strip().lower()
|
||||
if day in days:
|
||||
return days[day]
|
||||
if day.startswith("w") and day[1:].isdigit():
|
||||
day = int(day[1:])
|
||||
if not 0 <= day < 7:
|
||||
raise ValueError("Invalid weekday value while parsing day (expected [0-6]): '%d'" % day)
|
||||
else:
|
||||
day = None
|
||||
|
||||
return day
|
||||
|
||||
|
||||
def parse_time(time):
|
||||
time = time.strip()
|
||||
reg = re.compile(r"^[\d\.\:]+\s*(?:[ap]m)?$", flags=re.I)
|
||||
|
||||
if not reg.match(time):
|
||||
return None
|
||||
|
||||
formats = [
|
||||
"%H",
|
||||
"%H:%M",
|
||||
"%H:%M:%S",
|
||||
"%H:%M:%S.%f",
|
||||
"%I %p",
|
||||
"%I:%M %S",
|
||||
"%I:%M:%S %p",
|
||||
"%I:%M:%S.%f %p",
|
||||
]
|
||||
|
||||
for format_ in formats:
|
||||
try:
|
||||
dt = datetime.datetime.strptime(time, format_)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
return dt.time()
|
||||
|
||||
raise ValueError("Unrecognized format while parsing time: '%s'" % time)
|
||||
|
||||
|
||||
def parse_daytime(daytime):
|
||||
daytime = daytime.strip()
|
||||
reg = re.compile(r"^(.*?)\s+at\s+(.*)$", flags=re.I)
|
||||
|
||||
match = reg.match(daytime)
|
||||
if match:
|
||||
day, time = match.groups()
|
||||
else:
|
||||
day = time = daytime
|
||||
|
||||
try:
|
||||
parsed_day = parse_day(day)
|
||||
if match and parsed_day is None:
|
||||
raise ValueError("Unparsable day")
|
||||
except ValueError as e:
|
||||
raise ValueError("Invalid day while parsing daytime: '%s'" % day) from e
|
||||
|
||||
try:
|
||||
parsed_time = parse_time(time)
|
||||
if match and parsed_time is None:
|
||||
raise ValueError("Unparsable time")
|
||||
except ValueError as e:
|
||||
raise ValueError("Invalid time while parsing daytime: '%s'" % time) from e
|
||||
|
||||
if parsed_day is None and parsed_time is None:
|
||||
return None
|
||||
|
||||
return parsed_day, parsed_time
|
||||
Reference in New Issue
Block a user