theenglishway (time) 6 lat temu
commit
589cd6013d

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+.idea
+*.egg-info
+venv/
+*.pyc
+planning.json

+ 16 - 0
planner/__init__.py

@@ -0,0 +1,16 @@
+from tinydb import TinyDB
+from cerberus import schema_registry
+
+from .validator import DbValidator
+
+__author__ = """theenglishway"""
+__email__ = 'me@theenglishway.eu'
+__version__ = '0.1.0'
+
+db = TinyDB('planning.json', indent=4)
+validator = DbValidator(db)
+
+from planner.models import *
+
+Milestone.add_to_registry(schema_registry)
+Sprint.add_to_registry(schema_registry)

+ 56 - 0
planner/cli.py

@@ -0,0 +1,56 @@
+import click
+from planner.models import *
+
+
+@click.group()
+@click.option('-v', '--db', type=str, default='./planning.json', show_default=True)
+@click.pass_context
+def main(ctx, db):
+    """Basic CLI project-handling"""
+    ctx.ensure_object(dict)
+
+
+@main.group()
+@click.pass_context
+def milestone(ctx):
+    """Handle milestones"""
+    ctx.obj['model'] = Milestone
+
+@main.group()
+@click.pass_context
+def sprint(ctx):
+    """Handle sprints"""
+    ctx.obj['model'] = Sprint
+
+@click.command()
+@click.option('--name', prompt='Milestone name')
+@click.option('--description', prompt='Milestone description')
+@click.pass_context
+def new(ctx, name, description):
+    """Add a new milestone"""
+    model = ctx.obj['model']
+    data = {
+        'name': name,
+        'description': description
+    }
+    ok, m_doc = model.validate(data)
+    if ok :
+        m = model.create(m_doc)
+        m.save()
+
+@click.command()
+@click.pass_context
+def list(ctx):
+    """List"""
+    model = ctx.obj['model']
+    click.echo(model.list())
+
+
+milestone.add_command(new)
+milestone.add_command(list)
+
+sprint.add_command(new)
+sprint.add_command(list)
+
+if __name__ == '__main__':
+    main()

+ 7 - 0
planner/models/__init__.py

@@ -0,0 +1,7 @@
+from .milestones import Milestone
+from .sprints import Sprint
+
+__all__ = [
+    'Milestone',
+    'Sprint'
+]

+ 31 - 0
planner/models/milestones.py

@@ -0,0 +1,31 @@
+from dataclasses import dataclass
+from datetime import datetime
+
+from .mixins import SchemaMixin, DbMixin
+
+
+@dataclass(repr=False)
+class Milestone(SchemaMixin, DbMixin):
+    document: dict
+    name: str
+    description: str
+    date_expected: datetime = None
+    date_started: datetime = None
+
+    schema_yaml = """
+        name:
+            required: true
+            type: string
+            is_unique: milestone
+        description:
+            required: true
+            type: string
+        date_expected: 
+            type: date
+        date_started:
+            type: date
+    """
+
+    def __repr__(self):
+        return "{}(name='{}')".format(self.__class__.__qualname__, self.name)
+

+ 38 - 0
planner/models/mixins.py

@@ -0,0 +1,38 @@
+import yaml
+from planner import db, validator
+
+
+class SchemaMixin:
+    @classmethod
+    def class_name(cls):
+        return cls.__qualname__.lower()
+
+    @classmethod
+    def schema(cls):
+        return yaml.safe_load(cls.schema_yaml)
+
+    @classmethod
+    def validate(cls, data: dict):
+        ok = validator.validate(data, cls.schema())
+        return ok, validator.document if ok else validator.errors
+
+    @classmethod
+    def create(cls, document):
+        return cls(document=document, **(document))
+
+    @classmethod
+    def add_to_registry(cls, registry):
+        registry.add(cls.class_name(), cls.schema())
+
+
+class DbMixin:
+    @classmethod
+    def list(cls):
+        return [cls.get(doc) for doc in db.table(cls.class_name()).all()]
+
+    @classmethod
+    def get(cls, document):
+        return cls(document=document, **document)
+
+    def save(self):
+        db.table(self.class_name()).insert(self.document)

+ 48 - 0
planner/models/sprints.py

@@ -0,0 +1,48 @@
+from dataclasses import dataclass
+from datetime import datetime
+from copy import deepcopy
+
+from planner import db
+from .mixins import SchemaMixin, DbMixin
+from .milestones import Milestone
+
+
+@dataclass(repr=False)
+class Sprint(SchemaMixin, DbMixin):
+    document: dict
+    name: str
+    description: str
+    milestone: Milestone
+    date_expected: datetime = None
+    date_started: datetime = None
+
+    schema_yaml = """
+        name:
+            required: true
+            type: string
+            is_unique_with: 
+                table: sprint
+                fields:
+                  - milestone_id
+        description:
+            required: true
+            type: string
+        milestone_id:
+            required: true
+            is_fk: milestone
+    """
+
+    @classmethod
+    def get(cls, document):
+        kwargs = deepcopy(document)
+        milestone = Milestone.get(db.table(Milestone.class_name()).get(doc_id=kwargs.pop('milestone_id')))
+        return cls(document=document, milestone=milestone, **kwargs)
+
+    @classmethod
+    def create(cls, document):
+        kwargs = deepcopy(document)
+        milestone = db.table(Milestone.class_name()).get(doc_id=kwargs.pop('milestone_id'))
+        return cls(document=document, milestone=milestone, **kwargs)
+
+    def __repr__(self):
+        return "{}(name='{}', milestone='{}')".format(self.__class__.__qualname__, self.name, repr(self.milestone))

+ 39 - 0
planner/validator.py

@@ -0,0 +1,39 @@
+from tinydb import Query
+from cerberus import Validator
+
+
+class DbValidator(Validator):
+    def __init__(self, db, **kwargs):
+        super().__init__(**kwargs)
+        self.db = db
+
+    def _validate_is_fk(self, table_name, field, value):
+        """ {'type': 'string'} """
+        table = self.db.table(table_name)
+        if not table.get(doc_id=value):
+            self._error(field, "No value found in {}".format(table_name))
+
+    def _validate_is_unique(self, table_name, field, value):
+        """{'type': 'string'} """
+        Q = Query()
+        table = self.db.table(table_name)
+        if table.search(Q[field] == value):
+            self._error(field, "Value '{}' is already present in the database".format(value))
+
+    def _validate_is_unique_with(self, defs, field, value):
+        """ {'type': ('dict'),
+             'validator': 'dependencies'} """
+        def get_value(f):
+            return self._lookup_field(f)[1]
+
+        def make_query(*fields):
+            Q = Query()
+            for f in fields:
+                Q_temp = Query()
+                Q = Q & (Q_temp[f] == get_value(f))
+            return Q
+
+        table = self.db.table(defs['table'])
+        all_fields = field, *defs['fields']
+        if table.search(make_query(*all_fields)):
+            self._error(field, "Value '{}' is already present in the database".format(value))

+ 4 - 0
requirements.txt

@@ -0,0 +1,4 @@
+Cerberus
+Click
+PyYaml
+TinyDb

+ 56 - 0
setup.cfg

@@ -0,0 +1,56 @@
+[bumpversion]
+current_version = 0.1.0
+commit = True
+tag = True
+allow_dirty = True
+
+[metadata]
+name = Planner
+author = theenglishway
+author_email = me@theenglishway.eu
+version = attr: planner.__version__
+description = Plannet
+long_description = file: README.rst, CHANGELOG.rst
+license = BSD 3-Clause License
+classifiers = 
+	Programming Language :: Python :: 3
+	Programming Language :: Python :: 3.7
+url = https://code.theenglishway.eu/theenglishway-corp/humanizen/
+
+[options]
+zip_safe = False
+include_package_data = True
+packages = find:
+install_requires = 
+	flask
+
+[options.packages.find]
+exclude = 
+	tests
+	tests.*
+
+[options.entry_points]
+console_scripts =
+	planner = planner.cli:main
+
+
+[bumpversion:file:planner/__init__.py]
+search = __version__ = '{current_version}'
+replace = __version__ = '{new_version}'
+
+[tool:pytest]
+filterwarnings =
+    error
+    ignore::DeprecationWarning
+
+[coverage:run]
+branch = True
+source = planner, tests
+
+[coverage:report]
+precision = 2
+
+[coverage:paths]
+paths = 
+	.
+	/tmp/build/*/source/

+ 3 - 0
setup.py

@@ -0,0 +1,3 @@
+from setuptools import setup
+
+setup()