Kaynağa Gözat

Premier commit

fanch 1 yıl önce
ebeveyn
işleme
adb651f93e

+ 5 - 0
.gitignore

@@ -5,3 +5,8 @@
 /run/
 **.pyc
 **.sqlite3
+/src/djangotools.egg-info/
+/dist/
+/.idea/.gitignore
+/.idea/misc.xml
+/.idea/modules.xml

+ 1 - 0
MANIFEST.in

@@ -0,0 +1 @@
+recursive-include src *.*

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+# DjangoTools
+
+A short description of the project.

+ 25 - 0
setup.cfg

@@ -0,0 +1,25 @@
+[coverage:run]
+source = src
+
+[coverage:report]
+ignore_errors = False
+show_missing = True
+
+[coverage:xml]
+output = coverage.xml
+
+[tool:pytest]
+markers =
+    noci: marks tests so that they are not executed in continuous integration (jenkins)
+testpaths = tests/
+junit_family = xunit2
+console_output_style = progress
+log_level = DEBUG
+DJANGO_SETTINGS_MODULE = tests.settings
+
+[build_sphinx]
+; python setup.py build_sphinx  # Generate the HTML documentation in dist/docs/html
+source-dir = docs/source
+build-dir = dist/docs
+all_files = 1
+

+ 58 - 0
setup.py

@@ -0,0 +1,58 @@
+from setuptools import setup, find_packages
+from pathlib import Path
+
+install_requires = [
+    "django",
+    "requests",
+    "pycryptodome",
+    "unidecode"
+]
+
+tests_require = [
+    "fab",
+    "pytest"
+]
+
+def get_files(path):
+    root = Path(path)
+    queue = [root]
+    ret = []
+    while queue:
+        curr = queue.pop()
+        for file in curr.iterdir():
+            if file.is_file():
+                ret.append(str(file))
+            else:
+                queue.append(file)
+    return ret
+
+
+setup(
+    name="djangotools",
+    version="0.1.1",
+    description="A short description of the project.",
+    author="François GAUTRAIS",
+    install_requires=install_requires,
+    packages=find_packages("src"),
+    include_package_data=True,
+    zip_safe=False,
+    data_files=[
+        ("", ["README.md"]),
+    ],
+
+    test_suite="tests",
+    tests_require=tests_require,
+    extras_require={
+        "test": tests_require,
+        "pylint": ["pylint"],
+    },
+    scripts=[
+    ],
+
+    entry_points={
+        "console_scripts": [
+        ]
+    },
+    package_dir={"": "src"},
+)
+

+ 1 - 0
src/djangotools/__init__.py

@@ -0,0 +1 @@
+from djangotools.app import DjangoTools

+ 17 - 0
src/djangotools/app.py

@@ -0,0 +1,17 @@
+
+
+class _DjangoTools:
+
+    def __init__(self):
+        self._init = False
+        self.app = None
+        self.settings = None
+
+    def init(self, app, settings=None):
+        if self._init:
+            raise ValueError(f"")
+        self.app = app
+        self.settings = settings
+        self._init = True
+
+DjangoTools = _DjangoTools()

+ 3 - 0
src/djangotools/cmdline/__init__.py

@@ -0,0 +1,3 @@
+from djangotools.cmdline.common.command import Command, Argument, CommandData, Path, ThreadWrapper
+from djangotools.cmdline.common.args import ArgsParser
+from djangotools.cmdline.common.cron import Task, CronManager

+ 0 - 0
src/djangotools/cmdline/commands/__init__.py


+ 0 - 0
src/djangotools/cmdline/commands/backup/__init__.py


+ 227 - 0
src/djangotools/cmdline/commands/backup/base.py

@@ -0,0 +1,227 @@
+import datetime
+import json
+import re
+import shutil
+import tempfile
+import time
+from zipfile import ZipFile
+from pathlib import Path
+
+from djangotools.cmdline.common.command import Command
+from djangotools.common.date import CurrentDate
+from djangotools.config import app_dir
+
+JOURS = ["Lundi", "Mardi", "Mercredi",  "Jeudi", "Vendredi", "Samedi", "Dimanche"]
+MOIS = ["", "Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Aout", "Septembre", "Octobre", "Novembre", "Décembre"]
+
+def period_to_str(period, date):
+    if period == "daliy":
+        return f"journalière du {date}"
+    if period == "weekly":
+        return f"hebdomadaire de la semaine {date}"
+    if period == "monthly":
+        return f"mensuelle du mois de {date}"
+
+
+
+class BackupFile:
+
+    def __init__(self, dir, quiet):
+        self.quiet = quiet
+        self.file = Path(dir) / "backup.json"
+        self.log_file = Path(dir) / "backup.log"
+        self.log_messages = []
+        if self.file.is_file():
+            self.content = json.loads(self.file.read_text())
+        else:
+            self.content = {
+                "daily" : {
+                    "last" : None,
+                    "file" : None,
+                    "date" : None
+                },
+                "weekly" : {
+                    "last" : None,
+                    "file" : None,
+                    "date" : None
+                },
+                "monthly" : {
+                    "last" : None,
+                    "file" : None,
+                    "date" : None
+                }
+            }
+
+    @staticmethod
+    def get_wwek_number(now=None):
+        now = now or CurrentDate.now()
+        first_day = datetime.datetime(now.year, 1, 1)
+        nb = (now-first_day).days + first_day.weekday()
+        return nb // 7
+
+
+    @classmethod
+    def get_date(cls, period, now = None):
+        now = now or CurrentDate.now()
+        date = None
+        if period == "daily":
+            date =  JOURS[now.weekday()]
+        elif period == "weekly":
+            date = cls.get_wwek_number()
+        elif period == "monthly":
+            date = MOIS[now.month]
+        return date
+
+    def get_todo(self):
+        periods = ["daily", "weekly", "monthly"]
+        ret = []
+        for period in periods:
+            if self.content[period]["date"] != self.get_date(period):
+                ret.append(period)
+        return ret
+
+    def _log(self, type, *args):
+        line = None
+        if type=="backup":
+            line = f"{args[0]} : Sauvegarde {period_to_str(args[1], args[3])} dans {args[2]}"
+        elif type == "message":
+            line = f"{args[0]} : {args[1]}"
+        else:
+            raise ValueError(f"Erreur le type de message '{type}' est inconnu")
+        self.log_messages.append(line)
+        if not self.quiet: print(line)
+
+    def log(self, message):
+        now = CurrentDate.now()
+        t = now.strftime("%d/%m/%Y %H:%M:%S")
+        self._log("message", t, message)
+
+    def do_backup(self, period, file):
+        if period not in self.content:
+            raise ValueError(f"La période de backup '{period}' est inconnu")
+        now = CurrentDate.now()
+        t = now.strftime("%d/%m/%Y %H:%M:%S")
+        date = self.get_date(period)
+        self._log("backup", t, period, file, date)
+
+        self.content[period] = {
+            "last" : t,
+            "file" : str(file),
+            "date" : date
+        }
+
+    def save(self):
+        log = "\n".join(self.log_messages)+"\n"
+        with open(self.log_file, "a") as fd:
+            fd.write(log)
+        self.file.write_text(json.dumps(self.content, indent=2))
+
+class BackupCommand(Command):
+
+
+    def __init__(self):
+        super().__init__()
+        self.settings = app_dir
+        self.backup_dir = self.settings
+        self._lock_file = self.backup_dir / "lock"
+        self.backup_dir_daily = self.backup_dir / "daliy"
+        self.backup_dir_weekly = self.backup_dir / "weekly"
+        self.backup_dir_monthly = self.backup_dir / "monthly"
+
+        for dir in [self.backup_dir_daily, self.backup_dir_weekly, self.backup_dir_monthly, self._lock_file.parent]:
+            dir.mkdir(exist_ok=True, parents=True)
+
+
+    def get_name(self):
+        now = CurrentDate.now()
+        return f"{self.PREFIX}-{str(now.year).zfill(4)}_{str(now.month).zfill(2)}_{str(now.day).zfill(2)}.zip"
+
+    def get_date(self, file):
+        file = Path(file)
+        for x in re.findall(r"\d{4}_\d{2}_\d{2}.zip$", file.name):
+            x = [int(n) for n in x.replace(".zip", "").split("_")]
+            return datetime.datetime(*x)
+        return None
+
+
+
+    def gen_backup(self, output):
+        root = self.settings.DATA_DIR
+        queue = [root]
+        with ZipFile(output, "w") as zip:
+            while queue:
+                curr = queue.pop(0)
+                for file in curr.iterdir():
+                    if file.is_dir():
+                        if file == self.backup_dir: continue
+                        queue.append(file)
+                    else:
+                        zip.write(file, file.relative_to(root.parent))
+
+    def lock(self):
+        while self._lock_file.is_file():
+            time.sleep(1)
+        self._lock_file.touch()
+
+    def unlock(self):
+        if self._lock_file.is_file():
+            self._lock_file.unlink()
+
+
+    def __enter__(self):
+        self.lock()
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.unlock()
+
+    def clean(self):
+        now = CurrentDate.now()
+        for file in list(self.backup_dir_daily.iterdir()):
+            date = self.get_date(file)
+            if not date: continue
+            if date + datetime.timedelta(days=8) < now:
+                file.unlink()
+
+        for file in list(self.backup_dir_weekly.iterdir()):
+            date = self.get_date(file)
+            if not date: continue
+            if date + datetime.timedelta(days=32) < now:
+                file.unlink()
+
+        for file in list(self.backup_dir_monthly.iterdir()):
+            date = self.get_date(file)
+            if not date: continue
+            if date + datetime.timedelta(days=366) < now:
+                file.unlink()
+
+    def run(self, data):
+        with tempfile.TemporaryDirectory() as temp:
+            temp = Path(temp) / "data.zip"
+            self.clean()
+            back = BackupFile(self.backup_dir, data.quiet)
+
+            with self:
+                self.gen_backup(temp)
+                if not any(getattr(data, x) for x in ("daily", "weekly", "monthly") ):
+                    todo = set(back.get_todo())
+                    data.daily = data.daily or (todo & {"daily"})
+                    data.weekly = data.weekly or (todo & {"weekly"})
+                    data.monthly = data.monthly or (todo & {"monthly"})
+
+                name = self.get_name()
+
+                if data.daily:
+                    back.do_backup("daily", self.backup_dir_daily / name)
+                    shutil.copy(temp, self.backup_dir_daily / name)
+                if data.weekly:
+                    back.do_backup("weekly", self.backup_dir_weekly / name)
+                    shutil.copy(temp, self.backup_dir_weekly / name)
+                if data.monthly:
+                    back.do_backup("monthly", self.backup_dir_monthly / name)
+                    shutil.copy(temp, self.backup_dir_monthly / name)
+
+
+                back.save()
+
+
+

+ 0 - 0
src/djangotools/cmdline/common/__init__.py


+ 44 - 0
src/djangotools/cmdline/common/args.py

@@ -0,0 +1,44 @@
+import argparse
+import os
+import sys
+
+from djangotools.cmdline.common.command import Command
+from djangotools.config import load_app
+
+
+class ArgsParser(argparse.ArgumentParser):
+
+    default_settings_module = None
+
+    def __init__(self, commands_dirs):
+        super().__init__()
+        self.commands_dirs = commands_dirs
+        parser = self.add_subparsers(parser_class=argparse.ArgumentParser)
+        self.add_argument("-a", "--app-dir",  help="Dossier de données du serveur")
+        self.add_argument("-s", "--settings",  help="Module de settings à utiliser")
+
+        for cmd in Command.load(commands_dirs):
+            kwargs = {}
+            if cmd.HELP: kwargs["help"] = cmd.HELP
+            if cmd.ALIASES: kwargs["aliases"] = cmd.ALIASES
+            p = parser.add_parser(cmd.NAME, **kwargs)
+            for x in cmd.ARGUMENT or []:
+                p.add_argument(*x.args, **x.kwargs)
+            p.set_defaults(classe = cmd)
+
+    def parse(self, args):
+        ret = self.parse_args(args=args)
+        settings = ret.settings or self.default_settings_module
+
+        if settings:
+            os.environ["DJANGO_SETTINGS_MODULE"] = settings
+        else:
+            print(f"Aucune module de setting n'a été passé merci d'utiliser la variable"
+                  f" d'environnemnet DJANGO_SETTIGNS_MODULE ou le parmaètre --settings, -s", file =sys.stderr)
+            exit(-1)
+
+        load_app(settings, ret.app_dir)
+        if getattr(ret, "classe", None) is None:
+            self.print_help()
+            exit(-1)
+        ret.classe().run(ret)

+ 90 - 0
src/djangotools/cmdline/common/command.py

@@ -0,0 +1,90 @@
+import threading
+from pathlib import Path
+
+
+class CommandData:
+
+    def __init__(self, **kwargs):
+        for k, v in kwargs.items():
+            setattr(self, k, v)
+
+
+class Argument:
+    def __init__(self, *args, **kwargs):
+        self.args = args
+        self.kwargs = kwargs
+
+class ThreadWrapper(threading.Thread):
+
+    def __init__(self, fct):
+        super().__init__()
+        self.fct = fct
+
+    def run(self) -> None:
+        return self.fct()
+
+
+class Command:
+    NAME = None
+    HELP = None
+    ALIASES = []
+
+    ARGUMENT=None
+
+    def __init__(self):
+        pass
+
+    def execute(self, data):
+        return self.run(data)
+
+    def run(self, data):
+        raise NotImplementedError()
+
+    @classmethod
+    def thread_run(cls, data):
+        cmd = cls()
+        thread = ThreadWrapper(lambda x=data: cmd.run(x))
+        thread.start()
+        return thread
+
+    @staticmethod
+    def _load(root_module):
+        ret = set()
+        root = Path(root_module.__file__).parent
+        queue = [root]
+        root_module_str = root_module.__name__
+        assert root_module_str != "__main__"
+        while queue:
+            current = queue.pop()
+            for file in current.iterdir():
+                if file.name == "__pycache__": continue
+                if file.is_dir():
+                    queue.append(file)
+                    continue
+                if file.is_file() and file.name.endswith(".py"):
+                    module_name = root_module_str+"."+str(file.relative_to(root))[:-3].replace("/",".").replace(".py", "").replace("__init__", "")
+                    if module_name[-1] == ".": module_name = module_name[:-1]
+                    module = __import__(module_name, fromlist=['object'])
+                    for x in dir(module):
+                        data = getattr(module, x)
+                        if isinstance(data, type) and issubclass(data, Command) and data != Command and data.NAME is not None:
+                            ret.add(data)
+        return ret
+
+    @staticmethod
+    def load(commands_dirs=None):
+        if isinstance(commands_dirs, str): commands_dirs = [commands_dirs]
+        from djangotools.cmdline import commands
+        ret = set()
+        commands_dirs = [commands]+list(commands_dirs or [])
+        for commands_dir in commands_dirs:
+            if isinstance(commands_dir, str):
+                commands_dir = __import__(commands_dir, fromlist=['object'])
+            ret |= Command._load(commands_dir)
+
+        return ret
+
+
+
+
+

+ 141 - 0
src/djangotools/cmdline/common/cron.py

@@ -0,0 +1,141 @@
+import datetime
+import sys
+import time
+import traceback
+
+from djangotools.common.date import CurrentDate
+
+
+class CronTimer:
+
+    def __init__(self):
+        self.next_tick = None
+
+    def __lt__(self, other):
+        return self.next_tick < other.next_tick
+
+    def next(self):
+        raise NotImplementedError()
+
+    def poll(self):
+        if self.next_tick < time.time():
+            self.next()
+            return True
+        return False
+
+class CronTimerPeriod(CronTimer):
+    NAME="period"
+    def __init__(self, seconds):
+        super().__init__()
+        self.next_tick = time.time() + seconds
+        self.next()
+
+    def next(self):
+        if self.next_tick<=time.time():
+            self.next_tick = (self.next_tick or time.time()) + self.seconds
+
+
+class CronTimerDaily(CronTimer):
+
+    NAME="daily"
+    def __init__(self, hour, minute=0, second=0):
+        super().__init__()
+        self.hour = hour
+        self.minute = minute
+        self.second = second
+        self.next()
+
+    def next(self):
+        if not self.next_tick:
+            next_tick = CurrentDate.now()
+            next_tick = datetime.datetime(year=next_tick.year,
+                                          month=next_tick.month,
+                                          day=next_tick.day,
+                                          hour=self.hour,
+                                          minute=self.minute,
+                                          second=self.second,
+                                          tzinfo=next_tick.tzinfo)
+            self.next_tick = next_tick.timestamp()
+        if self.next_tick <= time.time():
+            self.next_tick+=24*60*60
+
+
+class Task:
+    NAME =  None
+    def __init__(self, *args, **kwargs):
+        self.timers = []
+        self.args = args
+        self.kwargs = kwargs
+
+    @property
+    def next_tick(self):
+        if self.timers:
+            return min(x.next_tick for x in self.timers)
+        return time.time()+3600*24
+
+    def run(self, data):
+        raise NotImplementedError()
+
+    def poll(self, data):
+        for timer in self.timers:
+            if timer.poll():
+                print(f"Execute task: {self.NAME}")
+                try:
+                    self.run(data)
+                except Exception as err:
+                    print(f"Erreur dans l'execution de la tache {self.NAME}", file=sys.stderr)
+                    traceback.print_exc()
+
+        return self.next_tick
+
+    def add_timer(self, timer):
+        self.timers.append(timer)
+
+class CronManager:
+    TASK_CLASS={
+
+    }
+
+    def __init__(self, resolution=3600):
+        self.resolution = resolution
+        self.tasks = []
+
+    def add_task(self, task):
+        if isinstance(task, Task):
+            self.tasks.append(task)
+        elif isinstance(task, (list, tuple, set)):
+            for x in task: self.add_task(x)
+        else:
+            name = task.get("name")
+            kwargs = task.get("kwargs") or {}
+            args = task.get("args") or []
+            timers = task.get("timers")
+            task_classe = CronManager.TASK_CLASS.get(name)
+            if task_classe is None:
+                raise ValueError(f"Erreur la classe de tache '{name}' est inconnu")
+            task = task_classe(*args, **kwargs)
+
+            for type, *args in timers:
+                timer_classe = None
+                if type == "daily":
+                    timer_classe = CronTimerDaily
+                else:
+                    timer_classe = CronTimerPeriod
+                task.add_timer(timer_classe(*args))
+            self.add_task(task)
+
+
+    def schedule(self, data):
+        while True:
+            next = time.time() + 3600*24
+            for task in self.tasks:
+                next = min(next, task.poll(data))
+
+            sleep = next - time.time()
+            time.sleep(sleep)
+
+    @staticmethod
+    def register(task_calsse):
+        CronManager.TASK_CLASS[task_calsse.NAME] = task_calsse
+
+

+ 0 - 0
src/djangotools/common/__init__.py


+ 77 - 0
src/djangotools/common/crypto.py

@@ -0,0 +1,77 @@
+import base64
+import binascii
+import tempfile
+from pathlib import Path
+from Crypto.Cipher import AES
+import hashlib
+
+from Crypto.Cipher import AES
+from Crypto.Util import Counter
+from Crypto import Random
+
+
+class AsciiCypher:
+
+    def __init__(self, key=None, file_key=None):
+        key_data = key if isinstance(key, bytes) else (key.encode("utf-8") if key is not None else None)
+        if key is None and file_key is None:
+            raise ValueError("One of 'key' or 'file_key' must be provided")
+        if key_data is None:
+            if not Path(file_key).exists():
+                raise FileNotFoundError('Le fichier de clé n\'existe pas')
+            key_data = Path(file_key).read_bytes()
+        hash = hashlib.sha256()
+        hash.update(key_data)
+        self.key = hash.digest()
+
+    def do_encrypt(self, data) -> str:
+        iv = Random.new().read(AES.block_size)
+        iv_int = int(binascii.hexlify(iv), 16)
+        ctr = Counter.new(AES.block_size * 8, initial_value=iv_int)
+        aes = AES.new(self.key, AES.MODE_CTR, counter=ctr)
+        ciphertext = aes.encrypt(data)
+        iv = base64.b64encode(iv).decode()
+        ciphertext = base64.b64encode(ciphertext).decode()
+        return f"{iv}|{ciphertext}"
+
+    def do_decrypt(self, data : str):
+        if "|" not in data:
+            raise ValueError("Bad value")
+        iv, ciphertext = [base64.b64decode(x) for x in data.split('|')]
+        iv_int = int.from_bytes(iv, byteorder='big')
+        ctr = Counter.new(AES.block_size * 8, initial_value=iv_int)
+        aes = AES.new(self.key, AES.MODE_CTR, counter=ctr)
+        plaintext = aes.decrypt(ciphertext)
+        return plaintext
+
+    @classmethod
+    def encrypt(cls, data, key=None, file=None):
+
+        try:
+            cypher = cls(key=key, file_key=file)
+        except ValueError as e:
+            print(e)
+            return None
+        if isinstance(data, str):
+            data = data.encode("utf-8")
+        return cypher.do_encrypt(data)
+
+    @classmethod
+    def decrypt(cls, data, key=None, file=None):
+
+        try:
+            cypher = cls(key=key, file_key=file)
+        except Exception as e:
+            print(e)
+            return None
+
+        return cypher.do_decrypt(data)
+
+    @classmethod
+    def decrypt_str(cls, data, key=None, file=None):
+        x = cls.decrypt(data, key, file)
+        if x:
+            return x.decode("utf-8")
+        return None
+
+

+ 49 - 0
src/djangotools/common/date.py

@@ -0,0 +1,49 @@
+import datetime
+
+
+def set_date(date):
+    CurrentDate.set_date(date)
+
+class CurrentDate:
+
+    value = None
+
+
+    _datetime = None
+
+    @classmethod
+    def now(cls, tz=None):
+        return cls.value or datetime.datetime.now(tz)
+    @classmethod
+    def today(cls):
+        return cls.value.date() if cls.value else datetime.date.today()
+
+    @classmethod
+    def set_value(cls, date):
+        return cls.set_date(date)
+
+    @classmethod
+    def set_date(cls, date):
+
+        if isinstance(date, str):
+            _datetime = datetime.datetime.strptime(date, "%d/%m/%y")
+
+
+        class _date_(datetime.date):
+            @classmethod
+            def today(cls):
+                return _datetime.date()
+
+        class _datetime_(datetime.datetime):
+            @classmethod
+            def now(cls, tz=None):
+                return _datetime
+
+        old_dt = datetime.datetime.origin if hasattr(datetime.datetime, "origin") else datetime.datetime
+        old_d = datetime.date.origin if hasattr(datetime.date, "origin") else datetime.date
+
+        datetime.datetime = _datetime_
+        datetime.datetime.origin = old_dt
+
+        datetime.date = _date_
+        datetime.date.origin = old_d

+ 62 - 0
src/djangotools/common/errors.py

@@ -0,0 +1,62 @@
+from djangotools.common import response
+
+
+class HttpCode:
+    OK = 200
+    CREATED = 201
+    NO_CONTENT = 204
+    BAD_REQUEST = 400
+    UNAUTHORIZED = 401
+    FORBIDDEN = 403
+    NOT_FOUND = 404
+    METHOD_NOT_ALLOWED = 405
+    NOT_ACCEPTABLE = 406
+
+
+class CustomException(Exception):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args)
+        msg = "Erreur l'exception '%s' ne définie pas l'attribut de classe '%s'"
+        if not hasattr(self, "HTTP_STATUS"):
+            raise BadDefinitionException(msg % (type(self).__name__, "HTTP_STATUS"))
+        if not hasattr(self, "CODE"):
+            raise BadDefinitionException(msg % (type(self).__name__, "CODE"))
+
+    def get_error(self):
+        return response.serv_json(self.HTTP_STATUS, self.HTTP_STATUS, self.CODE, str(self))
+
+class BadDefinitionException(CustomException):
+    HTTP_STATUS=400
+    CODE="bad defintion"
+
+class BadParameterException(CustomException):
+    HTTP_STATUS=400
+    CODE="bad parameter"
+
+
+class UnauthorizedException(CustomException):
+    HTTP_STATUS=401
+    CODE="Unauthorised"
+
+class NotFoundException(CustomException):
+    HTTP_STATUS=404
+    CODE="not found"
+
+class ImageNotFound(NotFoundException):
+    CODE="image not found"
+
+class TagNotFound(NotFoundException):
+    CODE="image not found"
+
+class RecursiveReference(NotFoundException):
+    HTTP_STATUS = 500
+    CODE="recursive reference"
+
+class Exists(NotFoundException):
+    HTTP_STATUS = 400
+    CODE="resource exist"
+
+
+class ElementNotFoundException(NotFoundException):
+    HTTP_STATUS=404
+    CODE="template elment not found"

+ 125 - 0
src/djangotools/common/path.py

@@ -0,0 +1,125 @@
+from pathlib import Path
+
+from djangotools.model import tools
+
+
+class UrlPath:
+
+    def __init__(self):
+        self.liste = [self]
+
+    def __truediv__(self, other):
+        root = UrlPath()
+        if isinstance(other, UrlPath):
+            root.liste = self.liste + other.liste
+        elif isinstance(other, str):
+            root.liste = self.liste + [ConstPath(other)]
+
+        return root
+
+    def iter_models(self):
+        for x in self.liste:
+            if isinstance(x, (ModelPath, AttributePath)):
+                yield x
+        return
+
+    def resolve(self):
+        return [self]
+
+    def get_wrapper(self, function):
+        return
+
+    def execute(self, manager):
+        root = Path(".")
+        for p in self.liste:
+            x =  p.resolve(manager)
+            if x is not None:
+                root = root / x
+        return root
+
+    def copy(self):
+        root = UrlPath()
+        root.liste = list(self.liste)
+        return root
+
+    def get_path(self):
+        liste = []
+        for x in self.liste:
+            liste.extend(x.resolve())
+        self.liste=liste
+        parts = [x.part for x in self.liste]
+        return "/".join([ x for x in parts if x])
+
+    @property
+    def part(self):
+        raise NotImplementedError()
+
+
+
+class ConstPath(UrlPath):
+
+    def __init__(self, string):
+        super().__init__()
+        self.string = string
+
+    @property
+    def part(self):
+        return self.string
+
+    def __repr__(self):
+        return self.string
+
+class AttributePath(UrlPath):
+
+    def __init__(self, model, attr):
+        super().__init__()
+        self.model = model
+        self.attr = attr
+        self.model_name = self.model if isinstance(self.model, str) else model.__name__
+        self.key = f"{self.model_name.lower()}_{self.attr}"
+        self.forward = None
+
+    @property
+    def part(self):
+        return f"<str:{self.key}>"
+
+
+class ModelPath(UrlPath):
+
+    def __init__(self, model):
+        super().__init__()
+        self.model = model
+
+    def __getitem__(self, item):
+        return self / AttributePath(self.model, item)
+
+    @property
+    def part(self):
+        return None
+
+class DeferredModelPath(UrlPath):
+    def __init__(self, model):
+        super().__init__()
+        self.model = model
+        self.attr = None
+
+
+    def resolve(self):
+        model = tools.get_model(self.model)
+        root = model._path_.resolve() if model._path_ else PointPath()
+
+        attribute = AttributePath(model, model._key_)
+        attribute.forward = self.attr
+        ret =  ( root / ConstPath(model.__name__.lower()) / attribute).liste
+        return ret
+
+    def __getitem__(self, item):
+        self.attr = item
+        return self
+
+
+
+class PointPath(UrlPath):
+    @property
+    def part(self):
+        return None

+ 46 - 0
src/djangotools/common/response.py

@@ -0,0 +1,46 @@
+from django.db import IntegrityError
+from django.http import JsonResponse
+
+
+
+def serv_json(httpcode, code, msg, data=None):
+    return JsonResponse({
+        "code": code,
+        "message": msg,
+        "data": data
+    }, status=httpcode)
+
+
+def serv_json_ok(data=None, msg="Success"):
+    return serv_json(200, 0, msg, data)
+
+
+def serv_json_bad_request(data=None, msg="Bad Request"):
+    return serv_json(400, 400, msg, data)
+
+
+def serv_json_unauthorized(data=None, msg="Unauthorised"):
+    return serv_json(401, 401, msg, data)
+
+
+def serv_json_forbidden(data=None, msg="Forbidden"):
+    return serv_json(403, 403, msg, data)
+
+def serv_json_not_logged(data = None):
+    msg = f"Vous devez vous logger pour effectuer cette action"
+    if data:
+        msg+=" : "+data
+    return serv_json(401, 401, "Unauthorised", msg)
+
+
+
+def serv_json_not_found(data=None, msg="Ressource not found"):
+    return serv_json(404, 404, msg, data)
+
+
+def serv_json_method_not_allowed(data=None, msg="Method Not Allowed"):
+    return serv_json(405, 405, msg, data)
+
+
+def serv_json_teapot(data=None, msg="I’m a teapot"):
+    return serv_json(418, 418, msg, data)

+ 104 - 0
src/djangotools/common/route.py

@@ -0,0 +1,104 @@
+from django.contrib.auth.models import User
+from django.db.models import Manager, Model, QuerySet
+from django.db.models.base import ModelBase
+
+from djangotools.common import response
+from djangotools.common.path import PointPath, ModelPath, AttributePath
+from djangotools.model.serializedmodel import SerializableModel
+
+
+class Route:
+    GET = "GET"
+    POST = "POST"
+    DELETE = "DELETE"
+    PUT = "PUT"
+    method  = {GET, POST, DELETE, PUT}
+    def __init__(self, route=None,  method=None):
+        self.method = method if method is not None else self.method
+        if isinstance(method, str): self.method = {method}
+        self.path = route or PointPath()
+
+    def __call__(self, *args, **kwargs):
+        return RegisteredRoute(self, args[0])
+
+    def __truediv__(self, other):
+        if isinstance(other, Route):
+            return Route(self.path / other.path, method=other.method)
+        if isinstance(other, RegisteredRoute):
+            return RegisteredRoute(self / other.route, other.callback)
+        raise Exception()
+
+
+
+class RegisteredRoute:
+    _default_user_id_ = 1
+    def __init__(self, path : Route, callback, is_manager=False, is_method=True, **options):
+        self.route = path if path is not None else Route()
+        self.callback = callback
+        self.is_manager = is_manager
+        self.is_method = is_method
+        self.options = options
+
+    def __call__(self, req, *args, **kwargs):
+        obj = None
+        if self._default_user_id_:
+            req.user = User.objects.get(id=self._default_user_id_)
+
+
+        for part in self.route.path.iter_models():
+            if obj is None:
+                obj = part.model.objects
+
+            if isinstance(part, ModelPath):
+                if not isinstance(obj, Manager):
+                    obj = part.model.objects
+            elif isinstance(part, AttributePath):
+                obj = obj.filter(**{part.attr: kwargs[part.key]})
+                if len(obj) > 2:
+                    return response.serv_json_bad_request(f"Trop de résultats pour {part.model_name}{part.attr} = {kwargs[part.key]}")
+                elif len(obj) == 0:
+                    return response.serv_json_not_found(f"Aucun résultat pour {part.model_name}.{part.attr} = {kwargs[part.key]}")
+                else:
+                    kwargs.pop(part.key)
+                    obj = obj.first()
+                    if part.forward:
+                        obj = getattr(obj, part.forward)
+            else:
+                raise Exception()
+
+
+        if self.is_manager:
+            if isinstance(obj, Model):
+                obj = obj.__class__.objects
+            elif isinstance(obj, ModelBase):
+                obj = obj.objects
+        if self.is_method:
+            ret = self.callback(obj, req, *args, **kwargs)
+        else:
+            ret = self.callback(req, obj, *args, **kwargs)
+
+        if isinstance(ret, QuerySet):
+            return [x.serialize() for x in ret]
+        if isinstance(ret, SerializableModel):
+            return ret.serialize()
+        if isinstance(ret, list):
+            ret = [x.serialize() if isinstance(x, SerializableModel) else x for x in ret]
+        return ret
+
+
+
+    def copy(self):
+        return RegisteredRoute(self.route, self.callback, is_manager=self.is_manager,
+                               is_method=self.is_method)
+
+class Get(Route):
+    method = {Route.GET}
+    
+class Post(Route):
+    method = {Route.POST}
+
+class Put(Route):
+    method = {Route.PUT}
+
+class Delete(Route):
+    method = {Route.DELETE}

+ 38 - 0
src/djangotools/common/types.py

@@ -0,0 +1,38 @@
+import datetime
+
+import unidecode as unidecode
+
+
+def parse_date(date):
+    if isinstance(date, datetime.date):
+        return date
+    if isinstance(date, datetime.datetime):
+        return date.date()
+    if isinstance(date, (int, float)):
+        return datetime.datetime.fromtimestamp(date).date()
+    if isinstance(date, str):
+        if date=="-":
+            return None
+        elif "-" in date and not "/" in date:
+            return datetime.datetime.strptime(date, "%Y-%m-%d").date()
+        elif "/" in date and not "-" in date:
+            if date.index("/")>2:
+                if len(date)==10:
+                    return datetime.datetime.strptime(date, "%Y/%m/%d").date()
+                elif len(date) == 8:
+                    return datetime.datetime.strptime(date, "%Y/%m/%d").date()
+            else:
+                if len(date)==10:
+                    return datetime.datetime.strptime(date, "%d/%m/%Y").date()
+                elif len(date) == 8:
+                    return datetime.datetime.strptime(date, "%d/%m/%y").date()
+    return None
+
+ALLOWED_CHARS = "ABCDEFGHIJKLMOPQRSTUVWXYZ 0123456789/-"
+
+def simple_string(s, filter_chars=True):
+    ret = unidecode.unidecode(s.upper())
+    if filter_chars:
+        return "".join([ x for x in ret if x in ALLOWED_CHARS])
+    else:
+        return ret

+ 6 - 0
src/djangotools/common/utils.py

@@ -0,0 +1,6 @@
+import json
+import random
+_id_chars=["abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-*+_"]
+
+def new_id(n=16):
+    return "".join([random.choice( _id_chars) for _ in range(n)])

+ 32 - 0
src/djangotools/config/__init__.py

@@ -0,0 +1,32 @@
+from djangotools.config.base import ConfigLoader
+import os
+
+settings = None
+app_dir = None
+
+def load_settings(module, base_dir=None, **kwargs):
+    global settings
+    global app_dir
+    settings = module
+    app_dir = ConfigLoader(module, base_dir, **kwargs)
+    app_dir.init()
+    return app_dir
+
+loaded = False
+
+def load_app(settings_module, app_dir=None, populate=True):
+    global loaded
+    if loaded: return
+    loaded = True
+
+    if app_dir:
+        os.environ["APP_DIR"] = str(app_dir)
+
+    if not "DJANGO_SETTINGS_MODULE" in os.environ:
+        os.environ.setdefault('DJANGO_SETTINGS_MODULE', settings_module)
+
+    from django.apps import apps
+    from django.conf import settings
+
+    if populate:
+        apps.populate(settings.INSTALLED_APPS)

+ 225 - 0
src/djangotools/config/base.py

@@ -0,0 +1,225 @@
+import json
+import os
+import sys
+from collections import OrderedDict
+from pathlib import Path
+
+from django.core.management.utils import get_random_secret_key
+
+
+
+class File(type(Path())):
+    def __new__(cls, *pathsegments):
+        return super().__new__(cls, *pathsegments)
+
+
+
+class Directory(type(Path())):
+    def __new__(cls, *pathsegments):
+        return super().__new__(cls, *pathsegments)
+
+
+class DeferredSetting:
+    def __init__(self, function):
+        self.function = function
+
+    def __call__(self, settings):
+        return self.function(settings)
+
+"""
+# app setting file
+
+app_dir = AppDir(globals(), ALLOW_AUTO_LOGIN=False, )
+
+
+
+"""
+
+disable_config_loader = False
+
+class NoConfigLoader:
+
+    def __enter__(self):
+        self.disable_config_loader = True
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.disable_config_loader = False
+
+def get_option(x, name, **kwargs):
+    if name not in x and "default" not in kwargs:
+        print(f"Erreur l'option {name} n'est pas définie dans la config", file=sys.stderr)
+        exit(-1)
+    return x.get(name, kwargs.get("default"))
+
+
+
+
+class ConfigLoader:
+    SECRET = "secret"
+    DATA = "data"
+    BACKUP = "backup"
+    CONFIG = "config"
+    LOGS = "logs"
+    RUN = "run"
+    RUN_FILE = f"{RUN}/app.pid"
+    DJANGO_SECRET_FILE = f"{SECRET}/django_secret"
+    CONFIG_FILE = f"{CONFIG}/config.json"
+
+    OTHERS_PATH = {}
+    DEFAULT_OPTIONS = OrderedDict([
+        ("LANGUAGE_CODE", 'fr-fr'),
+        ("TIME_ZONE", "Europe/Paris"),
+        ("USE_I18N", True),
+        ("USE_TZ", True),
+        ("DEBUG", False),
+        ("STATIC_URL", 'static/'),
+        ("CRON", []),
+        ("STATICFILES_DIRS",  DeferredSetting(lambda x: [get_option(x, "BASE_DIR") / "frontend" / "build" / "static"])),
+        ("TEMPLATES_DIR", DeferredSetting(lambda x: [get_option(x, "BASE_DIR") / "frontend" / "build"])),
+        ("ALLOWED_HOSTS", ["*"]),
+        ("ROOT_URLCONF", "djangotools.urls"),
+        ("CORS_ORIGIN_ALLOW_ALL", True),
+        ("DEFAULT_AUTO_FIELD", 'django.db.models.BigAutoField'),
+        ("AUTO_LOGIN", DeferredSetting(lambda x: os.environ["USER"])),
+        ("ALLOW_AUTO_LOGIN", DeferredSetting(lambda x: get_option(x, "DEBUG") and os.environ.get("ALLOW_AUTO_LOGIN", "False") != "False"))
+    ])
+
+    MANDATORY_OPTIONS = [
+        "BASE_DIR", "INSTALLED_APPS",
+    ]
+
+    def __init__(self, global_muodule, app_dir=None, **kwargs):
+        if disable_config_loader: return
+        self.kwargs = kwargs
+        self.global_muodule = global_muodule
+        if app_dir is not None:
+            self.app_dir = Directory(app_dir)
+        elif "APP_DIR" in os.environ:
+                self.app_dir = Directory(os.environ["APP_DIR"])
+        elif "app_dir" in global_muodule:
+            self.app_dir = Directory(global_muodule["app_dir"])
+        else:
+            raise ValueError("")
+
+        self.data_dir = Directory(self.app_dir / self.DATA)
+        self.secret_dir = Directory(self.app_dir / self.SECRET)
+        self.config_dir = Directory(self.app_dir /  self.CONFIG)
+        self.config_file = File(self.app_dir / self.CONFIG_FILE)
+        self.backup_dir = Directory(self.app_dir /  self.BACKUP)
+        self.logs_dir = Directory(self.app_dir /  self.LOGS)
+        self.run_dir = Directory(self.app_dir /  self.RUN)
+        self.run_file = File(self.app_dir /  self.RUN_FILE)
+        self.django_secret_file = File(self.app_dir / self.DJANGO_SECRET_FILE)
+
+        for path in [self.app_dir, self.data_dir, self.backup_dir, self.secret_dir, self.config_dir, self.logs_dir]:
+            if not path.exists(): path.mkdir(parents=True)
+
+        if not self.config_file.is_file():
+            self.config_file.write_text("{}")
+
+        for name, path in self.OTHERS_PATH.items():
+            if isinstance(path, Directory):
+                path.mkdir(exist_ok=True, parents=True)
+            setattr(self, name, path)
+
+    def _absolutize(self, data, root=None):
+        if root is None: root = self.global_muodule["BASE_DIR"]
+        dirs = []
+        for x in data:
+            if not isinstance(x, Path):
+                x = Path(x)
+            if not x.is_absolute():
+                x = root / x
+            dirs.append(x)
+        return dirs
+
+    def process_checks(self):
+        errors = []
+        for x in self.MANDATORY_OPTIONS:
+            if x not in self.global_muodule:
+                errors.append(x)
+
+        if errors:
+            [print(f"Erreur de configuration, l'option '{x}' n'est pas passé dans les settings", file=sys.stderr) for x in errors]
+            exit(-1)
+
+        if "SECRET_KEY" not in self.global_muodule:
+            if not self.django_secret_file.is_file():
+                self.django_secret_file.write_text(get_random_secret_key())
+            self.global_muodule["SECRET_KEY"] = self.django_secret_file.read_text()
+
+        if "DATABASES" not in self.global_muodule:
+            self.global_muodule["DATABASES"] = {
+                'default': {
+                    'ENGINE': 'django.db.backends.sqlite3',
+                    'NAME': self.data_dir / "db.sqlite3",
+                }
+            }
+
+        if "AUTH_PASSWORD_VALIDATORS" not in self.global_muodule:
+            self.global_muodule["AUTH_PASSWORD_VALIDATORS"] = [
+                {
+                    'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+                },
+                {
+                    'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+                },
+                {
+                    'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+                },
+                {
+                    'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+                },
+            ]
+
+        if "django.contrib.staticfiles" in self.global_muodule["INSTALLED_APPS"]:
+            self.global_muodule["STATICFILES_DIRS"] = self._absolutize(self.global_muodule["STATICFILES_DIRS"])
+
+
+
+        if "TEMPLATES" not in self.global_muodule:
+            templates_dirs = self._absolutize(self.global_muodule["TEMPLATES_DIR"])
+            self.global_muodule["TEMPLATES"] = [
+                {
+                    'BACKEND': 'django.template.backends.django.DjangoTemplates',
+                    'DIRS': templates_dirs,
+                    'APP_DIRS': True,
+                    'OPTIONS': {
+                        'context_processors': [
+                            'django.template.context_processors.debug',
+                            'django.template.context_processors.request',
+                            'django.contrib.auth.context_processors.auth',
+                            'django.contrib.messages.context_processors.messages',
+                        ],
+                    },
+                },
+            ]
+
+
+
+
+
+
+
+    def init(self):
+        if disable_config_loader: return
+
+        for k, v in self.kwargs.items():
+            if  k not in self.global_muodule:
+                self.global_muodule[k] = v
+
+
+        for k, v in self.DEFAULT_OPTIONS.items():
+            if isinstance(v, DeferredSetting):
+                v = v(self.global_muodule)
+            self.global_muodule.setdefault(k, v)
+
+        data = json.loads(self.config_file.read_text())
+        for k, v in data.items():
+            self.global_muodule[k] =  v
+
+
+
+        self.process_checks()
+
+

+ 0 - 0
src/djangotools/model/__init__.py


+ 201 - 0
src/djangotools/model/serializedmodel.py

@@ -0,0 +1,201 @@
+import datetime
+from collections.abc import Iterable
+from typing import Callable
+
+from django.db import models
+
+
+
+class Manager(models.Manager):
+    FIELD_CREATE_ITEM=()
+    def create_from_json(self, **kwargs):
+        return self.create(**kwargs)
+
+
+
+
+class Serializer:
+    def __init__(self, attr_name=None, from_json=None, to_json=None, validate=None, on_update=None):
+        self.attr_name = attr_name
+        self._from_json=from_json
+        self._to_json =to_json
+        self._validate = validate
+        self._on_update = on_update
+
+    def notify_update(self, obj, attrname):
+        if not self._on_update: return None
+        if isinstance(self._on_update, str):
+            getattr(obj, self._on_update)(attrname)
+        else:
+            self._on_update(obj, attrname)
+
+    def serialize(self, x, obj, context):
+        if self._to_json:
+            return self._to_json(x)
+        if x is None:
+            return None
+        if isinstance(x, (int, str, float, bytes, str)):
+            return x
+        if isinstance(x, datetime.date):
+            return f"{str(x.year).zfill(4)}/{str(x.month).zfill(2)}/{str(x.day).zfill(2)}"
+        if isinstance(x, datetime.datetime):
+            return f"{str(x.year).zfill(4)}/{str(x.month).zfill(2)}/{str(x.day).zfill(2)} {str(x.hour).zfill(2)}:{str(x.minute).zfill(2)}:{str(x.second).zfill(2)}.{str(x.microsecond).zfill(6)}"
+
+
+    def validate(self, x):
+        if self._validate: self._validate(x)
+
+    def get_value(self, x, old_value):
+        self.validate(x)
+        return self._from_json(x) if self._to_json else x
+
+
+class DateSerializer(Serializer):
+    def get_value(self, x, old_value):
+        x = "".join([a for  a in x.replace("-","/") if a in '0123456789/'])
+        return datetime.datetime.strptime(x, "%d/%m/%Y").date()
+class ManyToOneSerializer(Serializer):
+
+    def serialize(self, x, obj, context):
+        return [a.serialize() for a in x.all() ]
+
+class _BypassProperty:
+    pass
+
+BypassProperty = _BypassProperty()
+
+class FunctionSerilizer(Serializer):
+    def __init__(self, property, name=None):
+        name = name or property
+        super().__init__(name)
+        self._property_name = property
+
+    def serialize(self, x, obj, context):
+        return getattr(obj, self._property_name).fct(obj, context=context)
+
+
+class PropertySerializer(Serializer):
+    def serialize(self, x, obj, context):
+        return x
+
+
+def compare_dict(model_dict, form_dict):
+    for k, v in model_dict.items():
+        fv = form_dict.get(k)
+        if v is not None and v != "":
+            if not fv or fv!=v:
+                return False
+        elif fv is not None and fv != "":
+            if not v or fv!=v:
+                return False
+
+    return True
+
+def compare_list_dict(model, form):
+    model=list(model)
+    if len(model) != len(form):
+        return False
+
+    for i in range(len(model)):
+        if not compare_dict(model[i], form[i]): return False
+    return True
+
+class SerializeField:
+    def __init__(self, fct, *args, **kwargs):
+        self.args =args
+        self.kwargs = kwargs
+        self.fct = fct
+
+class serialize:
+
+    def __init__(self, *args, **kwargs):
+        if(args and len(args) and isinstance(args[0], Callable)):
+            self._is_simple = True
+            self.fct = args[0]
+            self.args =[]
+            self.kwargs = {}
+            kwargs["name"] = self.fct.__name__
+        else:
+            self.args =args
+            self.kwargs = kwargs
+            if args and len(args)==1: kwargs["name"] = args[0]
+            self.fct = None
+
+    def __call__(self, fct):
+        return SerializeField(fct, *self.args, **self.kwargs)
+
+class SerializableModel:
+    class Empty:
+        pass
+
+    empty=Empty()
+    _REGISTERED={}
+    serialize_fields = {}
+    bypass_fields = {}
+
+    @classmethod
+    def register(cls):
+        SerializableModel._REGISTERED[cls.__name__.lower()]=cls
+
+    @staticmethod
+    def resolve(name):
+        return SerializableModel._REGISTERED.get(name.lower())
+
+    def _get_decorated_fields(self):
+        fields = {k: getattr(self, k) for k in dir(self) if k!="objects" and  isinstance(getattr(self.__class__,k, None), (SerializeField, serialize))}
+        out = {}
+        for name, fct in fields.items():
+            if(fct.fct.__class__.__name__ == "property"):
+                raise ValueError("Cant do that on preperty....")
+                out[name] = PropertySerilizer(name)
+            if(fct.fct.__class__.__name__ == "function"):
+                out[name] = FunctionSerilizer(name, fct.kwargs.get("name") or name)
+        return out
+
+    def get_fields(self):
+        ser = {field.attname: Serializer() for field in self._meta.fields if field.attname not in self.bypass_fields}
+        ser.update(self.serialize_fields if hasattr(self, "serialize_fields") else {})
+        ser.update(self._get_decorated_fields())
+        return ser
+
+
+    def serialize(self, **context):
+        x=self.get_fields()
+        ret = {
+            v.attr_name or k: v.serialize(getattr(self, k), self, context) for k, v in x.items()
+        }
+        return {k:v for k, v in ret.items() if v != BypassProperty}
+
+    def update(self, x):
+        fields=self.get_fields()
+        reverse = {v.attr_name: k for k, v in fields.items() if v.attr_name}
+        for k, v in x.items():
+            serial = fields.get(k)
+            if serial:
+                dictname = attrname = k
+            elif k in reverse:
+                dictname = k
+                attrname = reverse[k]
+            else:
+                continue
+            if hasattr(self, attrname):
+                old_value = getattr(self, attrname)
+                if dictname in fields:
+                    attr = getattr(self, attrname)
+                    if attr and hasattr(attr, "model"):
+                        _old_value = [x.json for x in attr.all()]
+                        values = serial.get_value(v, old_value)
+                        if not compare_list_dict(_old_value, values):
+                            attr.all().delete()
+                            for x in values:
+                                attr.model.objects.create_from_parent(parent=self, **x)
+                            serial.notify_update(self, attrname)
+                    else:
+                        serial = fields.get(dictname)
+                        new_value = serial.get_value(v, old_value)
+                        if old_value != new_value:
+                            setattr(self, attrname, new_value)
+                            serial.notify_update(self, attrname)
+
+
+

+ 35 - 0
src/djangotools/model/tools.py

@@ -0,0 +1,35 @@
+from collections import defaultdict
+
+from django.apps import apps
+from django.db.models.base import ModelBase, Model
+
+
+def all_models(app=None):
+    if app:
+        return apps.all_models[app]
+    return app.all_models
+
+
+def iter_models(app=None):
+    if app:
+        return apps.all_models[app].values()
+    for app, models in apps.all_models.items():
+        for model in models.values():
+            yield model
+
+def iter_models_items(app=None):
+    if app:
+        return apps.all_models[app].items()
+    for app, models in apps.all_models.items():
+        for x in models.items():
+            yield x
+
+def get_model(name, app=None):
+    name = name.lower()
+    if isinstance(name, ModelBase):
+        return name
+    if app is not None: return apps.all_models[app][name]
+    for _, models in apps.all_models.items():
+        if name in models:
+            return models[name]
+    raise ValueError(f"Impossible de trouver le model {name}")

+ 59 - 0
src/djangotools/run.py

@@ -0,0 +1,59 @@
+from django.contrib.staticfiles.management.commands.runserver import Command as RunServer
+from django.core.management.commands.migrate import Command as MigrateCommand
+
+from djangotools.config import load_app
+
+
+class Server(RunServer):
+    default_options = {
+        'verbosity': 1,
+        'settings': None,
+        'pythonpath': None,
+        'traceback': True,
+        'no_color': False,
+        'force_color': True,
+        'addrport': "localhost:8000",
+        'use_ipv6': False,
+        'use_threading': False,
+        'use_reloader': False,
+        'skip_checks': False,
+        'use_static_handler': True,
+        'insecure_serving': True
+    }
+
+
+    def handle(self, **options):
+        for k, v in self.default_options.items():
+            options.setdefault(k, v)
+
+        super().handle(**options)
+
+
+class Migrate(MigrateCommand):
+
+    default_options = {
+        'verbosity': 1,
+        "interactive" : False,
+        "skip_checks" : False,
+        "database" : "default",
+        "prune" : False,
+        "check_unapplied" : False,
+        "run_syncdb" : False,
+        "noinput" : True,
+        "fake" : False,
+        "fake_initial" : False,
+        "plan" : False,
+        "app_label" : None,
+
+    }
+    def handle(self, **options):
+        for k, v in self.default_options.items():
+            options.setdefault(k, v)
+
+        super().handle(**options)
+
+if __name__ == "__main__":
+    cmd = Server()
+    load_app("banque.banque.settings")
+    cmd.handle()
+

+ 19 - 0
src/djangotools/urls.py

@@ -0,0 +1,19 @@
+from django.urls import path
+
+from djangotools.view.auto_path import AutoPathManager
+from djangotools.view.router import Router
+from django.contrib import admin
+
+class RouterIterator:
+
+    def __init__(self):
+        self._cache = None
+
+    def __iter__(self):
+        if not self._cache:
+            print("loaded")
+            self._cache= [path('admin/', admin.site.urls)] + AutoPathManager.get_instance().get_pathes() + Router.get_pathes()
+        return iter(self._cache)
+
+
+urlpatterns = RouterIterator()

+ 0 - 0
src/djangotools/view/__init__.py


+ 126 - 0
src/djangotools/view/auto_path.py

@@ -0,0 +1,126 @@
+import json
+from collections import defaultdict
+
+from django.http import HttpRequest, HttpResponse
+from django.urls import path
+
+from djangotools.common import response
+from djangotools.common.path import PointPath, ConstPath, AttributePath, UrlPath, ModelPath
+from djangotools.common.route import RegisteredRoute, Post, Delete, Route, Get, Put
+from djangotools.model import tools
+
+
+class AutoPath:
+
+    _path_ = None
+    _key_ = "id"
+    _registered_ = []
+    _inited = False
+
+
+    def _auto_path_update(self, req):
+
+        self.update(json.loads(req.body))
+        self.save()
+        return self
+
+    def _auto_path_get(self, req):
+        return self
+    def _auto_path_list(self, req):
+        return self.all()
+
+    def _auto_path_delete(self, req):
+        self.delete()
+        return True
+    def _auto_path_create(self, req):
+        try:
+            return self.create(**json.loads(req.body))
+        except Exception as err:
+            return err
+
+    @classmethod
+    def init(cls, manager):
+        if cls._inited: return
+
+        root = cls._path_
+        if root is None:
+            root = PointPath()
+
+        root = root / ConstPath(cls.__name__.lower()) / AttributePath(cls, cls._key_)
+
+        manager.add(RegisteredRoute(Post(root), cls._auto_path_update))
+        manager.add(RegisteredRoute(Delete(root), cls._auto_path_delete))
+        manager.add(RegisteredRoute(Get(root), cls._auto_path_get))
+
+        for k in dir(cls):
+            v = getattr(cls, k)
+            if isinstance(v, RegisteredRoute) and v != root:
+                manager.add(Route(root) / v)
+
+        if isinstance(root.liste[-1], AttributePath):
+            _root = UrlPath()
+            _root.liste = root.liste[:-1]
+            _root = _root / ModelPath(cls)
+
+        manager.add(RegisteredRoute(Get(_root), cls._auto_path_list, True))
+        manager.add(RegisteredRoute(Put(_root), cls._auto_path_create, True))
+
+        for k in  dir(cls.objects):
+            v = getattr(cls.objects, k)
+            if isinstance(v, RegisteredRoute) and v != _root:
+                rr = Route(_root) / v
+                rr.is_manager = True
+                manager.add(rr)
+
+        cls._inited = True
+
+
+
+
+
+
+class AutoPathManager:
+    _ref = None
+
+    def __init__(self, app=None):
+        self._models = {}
+        for name, model in tools.iter_models_items():
+            if isinstance(model, type) and issubclass(model, AutoPath):
+                self._models[model.__name__] = model
+        self.routes = defaultdict(dict)
+
+
+    def _init(self):
+        for name, model in self._models.items():
+            model.init(self)
+        return self
+
+    def add(self, registerd_route, override=True):
+        for method in registerd_route.route.method:
+            path = registerd_route.route.path.get_path()
+            if method in self.routes[path]:
+                if not override:
+                    raise ValueError(f"La route {path} est déja défini avec {method}")
+            self.routes[path][method] = registerd_route
+
+    @classmethod
+    def get_instance(cls):
+        if cls._ref is None:
+            cls._ref = AutoPathManager()
+            cls._ref._init()
+        return cls._ref
+
+    def get_pathes(self):
+        pathes = []
+        for route, methods in self.routes.items():
+            def wrapper(req : HttpRequest, *args, methods=methods, **kwargs):
+                method = req.method
+                if method not in methods:
+                    return response.serv_json_not_found(f"Method {method} on {req.path}")
+                ret = methods[method](req, *args, **kwargs)
+                if isinstance(ret, HttpResponse):
+                    return ret
+                return response.serv_json_ok(ret)
+
+            pathes.append(path(route, wrapper))
+        return pathes

+ 91 - 0
src/djangotools/view/router.py

@@ -0,0 +1,91 @@
+from collections import defaultdict
+from functools import partial
+
+from django.db.models.base import ModelBase
+from django.http import HttpRequest, HttpResponse
+from django.urls import path, URLPattern
+
+from djangotools.common import response
+from djangotools.common.route import RegisteredRoute, Route
+from djangotools import  DjangoTools
+from djangotools.model.serializedmodel import SerializableModel
+from djangotools.model.tools import iter_models
+
+from django.urls import path
+class _Router:
+
+    class _Route(Route):
+        def __call__(self, fct, **options):
+            rr =RegisteredRoute(self, fct, is_method=False, **options)
+            Router.add(rr)
+            return rr
+
+    def __init__(self):
+        self._models = {}
+        for  model in iter_models(DjangoTools.app):
+            if isinstance(model, type) and issubclass(model, SerializableModel):
+                self._models[model.__name__] = model
+        self.routes = defaultdict(dict)
+        self._raw_pathes = []
+
+    def route(self, route=None, method=None, **options):
+        return self._Route(route, method, **options)
+
+    def get(self, path, **options):
+        return self.route(path, Route.GET, **options)
+
+    def post(self, path, **options):
+        return self.route(path, Route.POST, **options)
+
+    def put(self, path, **options):
+        return self.route(path, Route.PUT, **options)
+
+
+    def delete(self, path, **options):
+        return self.route(path, Route.DELETE, **options)
+
+
+    def __iadd__(self, other):
+        self.add(other)
+        return self
+
+    def add(self, registerd_route, override=True):
+        print("add", registerd_route)
+        if isinstance(registerd_route, RegisteredRoute):
+            for method in registerd_route.route.method:
+                current_path = registerd_route.route.path.get_path()
+                if method in self.routes[current_path]:
+                    if not override:
+                        raise ValueError(f"La route {current_path} est déja défini avec {method}")
+                self.routes[current_path][method] = registerd_route
+        elif isinstance(registerd_route, (list, tuple)):
+            [self.add(x) for x in registerd_route]
+        elif isinstance(registerd_route, (partial, URLPattern)):
+            self._raw_pathes.append(registerd_route)
+        else:
+            print(registerd_route)
+            raise Exception()
+
+    def resolve_model(self, model):
+        if isinstance(model, ModelBase): return model
+        return self._models[model]
+
+    def get_pathes(self):
+        pathes = []
+        for route, methods in self.routes.items():
+            def wrapper(req: HttpRequest, *args, methods=methods, **kwargs):
+                method = req.method
+                if method not in methods:
+                    return response.serv_json_not_found(f"Method {method} on {req.path}")
+                ret = methods[method](req, *args, **kwargs)
+                if isinstance(ret, HttpResponse):
+                    return ret
+                return response.serv_json_ok(ret)
+
+            pathes.append(path(route, wrapper))
+        return pathes + self._raw_pathes
+
+
+
+
+Router = _Router()

+ 0 - 0
tests/__init__.py


+ 0 - 0
tests/test_a.py


+ 1 - 0
todo

@@ -0,0 +1 @@
+- gérer les config personnalisabel