mirror of
https://github.com/almet/copanier.git
synced 2025-04-28 11:32:38 +02:00
non working MongoDB custom ODM
This commit is contained in:
commit
b25d8a28c8
10 changed files with 465 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
__pycache__
|
||||
*.egg-info
|
||||
tmp/
|
190
kaba/__init__.py
Normal file
190
kaba/__init__.py
Normal file
|
@ -0,0 +1,190 @@
|
|||
import os
|
||||
from time import perf_counter
|
||||
|
||||
import ujson as json
|
||||
import hupper
|
||||
import minicli
|
||||
from bson import ObjectId
|
||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||
from pymongo import MongoClient
|
||||
from roll import Roll, Response
|
||||
from roll.extensions import cors, options, traceback, simple_server
|
||||
|
||||
from .base import Document, Str, Float, Array, Email, Int, Reference, Datetime, Mapping
|
||||
|
||||
|
||||
class Response(Response):
|
||||
def html(self, template_name, *args, **kwargs):
|
||||
self.headers["Content-Type"] = "text/html; charset=utf-8"
|
||||
context = app.context()
|
||||
context.update(kwargs)
|
||||
context["request"] = self.request
|
||||
if self.request.cookies.get("message"):
|
||||
context["message"] = json.loads(self.request.cookies["message"])
|
||||
self.cookies.set("message", "")
|
||||
self.body = env.get_template(template_name).render(*args, **context)
|
||||
|
||||
|
||||
class Roll(Roll):
|
||||
Response = Response
|
||||
|
||||
_context_func = []
|
||||
|
||||
def context(self):
|
||||
context = {}
|
||||
for func in self._context_func:
|
||||
context.update(func())
|
||||
return context
|
||||
|
||||
def register_context(self, func):
|
||||
self._context_func.append(func)
|
||||
|
||||
|
||||
env = Environment(
|
||||
loader=PackageLoader("kaba", "templates"), autoescape=select_autoescape(["kaba"])
|
||||
)
|
||||
|
||||
|
||||
class Producer(Document):
|
||||
__collection__ = "producers"
|
||||
name = Str(required=True)
|
||||
|
||||
@property
|
||||
def products(self):
|
||||
return Product.find(producer=self._id)
|
||||
|
||||
|
||||
class Product(Document):
|
||||
__collection__ = "products"
|
||||
producer = Reference(Producer, required=True)
|
||||
name = Str(required=True)
|
||||
ref = Str(required=True)
|
||||
description = Str()
|
||||
price = Float(required=True)
|
||||
|
||||
|
||||
class Person(Document):
|
||||
__collection__ = "persons"
|
||||
first_name = Str()
|
||||
last_name = Str()
|
||||
email = Email()
|
||||
|
||||
|
||||
class ProductOrder(Document):
|
||||
ref = Str()
|
||||
wanted = Int()
|
||||
ordered = Int()
|
||||
|
||||
|
||||
class PersonOrder(Document):
|
||||
person = Str()
|
||||
products = Array(ProductOrder)
|
||||
|
||||
|
||||
class Order(Document):
|
||||
__collection__ = "orders"
|
||||
when = Datetime(required=True)
|
||||
where = Str()
|
||||
producer = Reference(Producer, required=True)
|
||||
products = Array(Product)
|
||||
orders = Mapping(Str, PersonOrder)
|
||||
|
||||
|
||||
app = Roll()
|
||||
cors(app, methods="*", headers="*")
|
||||
options(app)
|
||||
|
||||
|
||||
@app.listen("request")
|
||||
async def attach_request(request, response):
|
||||
response.request = request
|
||||
|
||||
|
||||
@app.listen("startup")
|
||||
async def on_startup():
|
||||
connect()
|
||||
|
||||
|
||||
@app.route("/", methods=["GET"])
|
||||
async def home(request, response):
|
||||
response.html("home.html", {"orders": Order.find()})
|
||||
|
||||
|
||||
@app.route("/commande/{order_id}", methods=["GET"])
|
||||
async def get_order(request, response, order_id):
|
||||
order = Order.find_one(_id=ObjectId(order_id))
|
||||
response.html(
|
||||
"order.html",
|
||||
{"order": order, "person": request.query.get("email"), "person_order": None},
|
||||
)
|
||||
|
||||
|
||||
@app.route("/commande/{order_id}", methods=["POST"])
|
||||
async def place_order(request, response, order_id):
|
||||
order = Order.find_one(_id=ObjectId(order_id))
|
||||
email = request.query.get("email")
|
||||
person_order = PersonOrder(person=email)
|
||||
form = request.form
|
||||
for product in order.products:
|
||||
quantity = form.int(product.ref, 0)
|
||||
if quantity:
|
||||
person_order.products.append(ProductOrder(ref=product.ref, wanted=quantity))
|
||||
if not order.orders:
|
||||
order.orders = {}
|
||||
order.orders[email] = person_order
|
||||
order.replace_one()
|
||||
response.headers["Location"] = request.url.decode()
|
||||
response.status = 302
|
||||
|
||||
|
||||
def connect():
|
||||
db = os.environ.get("KABA_DB", "mongodb://localhost/kaba")
|
||||
client = MongoClient(db)
|
||||
db = client.get_database()
|
||||
Producer.bind(db)
|
||||
Product.bind(db)
|
||||
Order.bind(db)
|
||||
Person.bind(db)
|
||||
return client
|
||||
|
||||
|
||||
@minicli.cli()
|
||||
def shell():
|
||||
"""Run an ipython already connected to Mongo."""
|
||||
try:
|
||||
from IPython import start_ipython
|
||||
except ImportError:
|
||||
print('IPython is not installed. Type "pip install ipython"')
|
||||
else:
|
||||
start_ipython(
|
||||
argv=[],
|
||||
user_ns={
|
||||
"Producer": Producer,
|
||||
"app": app,
|
||||
"Product": Product,
|
||||
"Person": Person,
|
||||
"Order": Order,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@minicli.wrap
|
||||
def cli_wrapper():
|
||||
connect()
|
||||
start = perf_counter()
|
||||
yield
|
||||
elapsed = perf_counter() - start
|
||||
print(f"Done in {elapsed:.5f} seconds.")
|
||||
|
||||
|
||||
@minicli.cli
|
||||
def serve(reload=False):
|
||||
"""Run a web server (for development only)."""
|
||||
if reload:
|
||||
hupper.start_reloader("kaba.serve")
|
||||
traceback(app)
|
||||
simple_server(app, port=2244)
|
||||
|
||||
|
||||
def main():
|
||||
minicli.run()
|
169
kaba/base.py
Normal file
169
kaba/base.py
Normal file
|
@ -0,0 +1,169 @@
|
|||
from datetime import datetime
|
||||
|
||||
from bson import ObjectId
|
||||
|
||||
|
||||
class classproperty:
|
||||
def __init__(self, f):
|
||||
self.f = f
|
||||
|
||||
def __get__(self, obj, owner):
|
||||
return self.f(owner)
|
||||
|
||||
|
||||
class DoesNotExist(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class Field:
|
||||
|
||||
name = None
|
||||
coerce = None
|
||||
|
||||
def __init__(self, choices=[], required=False, default=None):
|
||||
self.choices = choices
|
||||
self.required = required
|
||||
self.default = default
|
||||
|
||||
def __get__(self, obj, type=None):
|
||||
if obj is None:
|
||||
return self
|
||||
value = obj.get(self.name)
|
||||
return value
|
||||
|
||||
def __set__(self, obj, value):
|
||||
print("set", value, id(value))
|
||||
value = self.coerce(value)
|
||||
print("set after", value, id(value))
|
||||
obj[self.name] = value
|
||||
|
||||
|
||||
class Str(Field):
|
||||
coerce = str
|
||||
|
||||
|
||||
class Float(Field):
|
||||
coerce = float
|
||||
|
||||
|
||||
class Int(Field):
|
||||
coerce = int
|
||||
|
||||
|
||||
class Datetime(Field):
|
||||
def coerce(self, value):
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
if isinstance(value, int):
|
||||
return datetime.fromtimestamp(value)
|
||||
|
||||
|
||||
class Email(Field):
|
||||
def coerce(self, value):
|
||||
# TODO proper validation
|
||||
if "@" not in value:
|
||||
raise ValueError(f"Invalid value for email: {value}")
|
||||
return value
|
||||
|
||||
|
||||
class Reference(Field):
|
||||
|
||||
def coerce(self, value):
|
||||
if isinstance(value, dict):
|
||||
value = value["_id"]
|
||||
return ObjectId(value)
|
||||
|
||||
def __init__(self, document, *args, **kwargs):
|
||||
self.document = document
|
||||
return super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class Dict(Field):
|
||||
coerce = dict
|
||||
|
||||
|
||||
class Mapping(Field):
|
||||
def __init__(self, key_field, value_field, *args, **kwargs):
|
||||
self.key_field = key_field
|
||||
self.value_field = value_field
|
||||
return super().__init__(*args, **kwargs)
|
||||
|
||||
def coerce(self, value):
|
||||
print("coerce raw", value, id(value))
|
||||
if value is None:
|
||||
value = {}
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{value} is not a dict")
|
||||
print("coerce", value, id(value))
|
||||
return {
|
||||
self.key_field.coerce(k): self.value_field.coerce(v)
|
||||
for k, v in value.items()
|
||||
}
|
||||
|
||||
|
||||
class Array(Field):
|
||||
def __init__(self, type, *args, **kwargs):
|
||||
self.coerce = type
|
||||
return super().__init__(*args, **kwargs)
|
||||
|
||||
def __get__(self, obj, type=None):
|
||||
if obj is None:
|
||||
return self
|
||||
value = obj.get(self.name)
|
||||
if value is None:
|
||||
value = []
|
||||
self.__set__(value)
|
||||
return value
|
||||
|
||||
def __set__(self, obj, value):
|
||||
obj[self.name] = [self.coerce(v) for v in value or []]
|
||||
|
||||
|
||||
class MetaDocument(type):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
for attr_name, attr_value in attrs.items():
|
||||
if not isinstance(attr_value, Field):
|
||||
continue
|
||||
attr_value.name = attr_name
|
||||
return super().__new__(cls, name, bases, attrs)
|
||||
|
||||
|
||||
class Document(dict, metaclass=MetaDocument):
|
||||
__db__ = None
|
||||
__collection__ = None
|
||||
|
||||
# def __repr__(self):
|
||||
# return f"<{self.__class__.__name__} {self._id}>"
|
||||
|
||||
@property
|
||||
def _id(self):
|
||||
return self["_id"]
|
||||
|
||||
def insert_one(self):
|
||||
self.collection.insert_one(self)
|
||||
return self
|
||||
|
||||
def replace_one(self):
|
||||
self.collection.replace_one({"_id": self._id}, self)
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def find_one(cls, **kwargs):
|
||||
raw = cls.collection.find_one(kwargs)
|
||||
if not raw:
|
||||
raise DoesNotExist
|
||||
return cls(**raw)
|
||||
|
||||
@classmethod
|
||||
def find(cls, **kwargs):
|
||||
for raw in cls.collection.find(kwargs):
|
||||
yield cls(**raw)
|
||||
|
||||
@classproperty
|
||||
def collection(cls):
|
||||
assert cls.__collection__ is not None, f"You must define a {cls}.__collection__"
|
||||
return cls.__db__[cls.__collection__]
|
||||
|
||||
@classmethod
|
||||
def bind(cls, db):
|
||||
cls.__db__ = db
|
17
kaba/templates/base.html
Normal file
17
kaba/templates/base.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{% if title %}{{ title }} - {% endif %}Commandes Epinamap</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" type="text/css">
|
||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.4.1/css/bulma.css">
|
||||
{% block head %}
|
||||
{% endblock head %}
|
||||
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}
|
||||
{% endblock body %}
|
||||
</body>
|
||||
</html>
|
14
kaba/templates/home.html
Normal file
14
kaba/templates/home.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
{% for order in orders %}
|
||||
<p>Producteur: {{ order.producer }}</p>
|
||||
<p>Lieu: {{ order.where }}</p>
|
||||
<p>Date: {{ order.when }}</p>
|
||||
<form action="/commande/{{ order._id }}">
|
||||
<label for="email">Participer à la commande</label>
|
||||
<input type="text" name="email">
|
||||
<input type="submit" value="Commander">
|
||||
</form>
|
||||
{% endfor %}
|
||||
{% endblock body %}
|
18
kaba/templates/order.html
Normal file
18
kaba/templates/order.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<p>Producteur: {{ order.producer }}</p>
|
||||
<p>Lieu: {{ order.where }}</p>
|
||||
<p>Date: {{ order.when }}</p>
|
||||
<p>Commande pour {{ person }}</p>
|
||||
<form method="post">
|
||||
<table>
|
||||
<tr><th>Produit</th><th>Prix</th><th>Quantité</th></tr>
|
||||
{% for product in order.products %}
|
||||
<tr><th>{{ product.name }}</th><td>{{ product.price }} €</td><td><input type="number" name="{{ product.ref }}"></td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<input type="hidden" name="email" value="{{ person }}">
|
||||
<input type="submit" value="Valider ma commande">
|
||||
</form>
|
||||
{% endblock body %}
|
7
setup.cfg
Normal file
7
setup.cfg
Normal file
|
@ -0,0 +1,7 @@
|
|||
[metadata]
|
||||
name = kaba
|
||||
version = 0.0.1
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
kaba = kaba:main
|
2
setup.py
Normal file
2
setup.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from setuptools import setup
|
||||
setup()
|
28
tests/conftest.py
Normal file
28
tests/conftest.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
import os
|
||||
|
||||
import pytest
|
||||
from roll.extensions import traceback
|
||||
|
||||
# Before loading any eurordis module.
|
||||
os.environ["KABA_DB"] = "mongodb://localhost/kaba_test"
|
||||
|
||||
from kaba import app as kaba_app
|
||||
from kaba import connect, Producer
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
client = connect()
|
||||
client.drop_database("test_kaba")
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
# assert get_db().name == "test_eurordis"
|
||||
for cls in [Producer]:
|
||||
collection = cls.collection
|
||||
collection.drop()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app(): # Requested by Roll testing utilities.
|
||||
traceback(kaba_app)
|
||||
return kaba_app
|
17
tests/test_models.py
Normal file
17
tests/test_models.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from kaba import Producer, Order, Product
|
||||
|
||||
|
||||
def test_can_create_producer():
|
||||
producer = Producer(name="Andines")
|
||||
producer.insert_one()
|
||||
assert producer.name == "Andines"
|
||||
retrieved = Producer.find_one(name="Andines")
|
||||
assert retrieved.name == producer.name
|
||||
assert retrieved._id == producer._id
|
||||
|
||||
|
||||
def test_can_create_order():
|
||||
order = Order(products=[Product(name="riz", price="2.4")])
|
||||
order.insert_one()
|
||||
retrieved = Order.find_one(_id=order._id)
|
||||
assert retrieved.products[0].name == "riz"
|
Loading…
Reference in a new issue