Skip to content

Commit 5b002b8

Browse files
Initial commit
0 parents  commit 5b002b8

18 files changed

+502
-0
lines changed

.coveragerc

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[run]
2+
branch = False
3+
omit=
4+
pydantic_mongo/__init__.py

.flake8

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[flake8]
2+
max-line-length=140

.github/workflows/test.yml

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Tests
2+
on: push
3+
jobs:
4+
build:
5+
runs-on: ubuntu-latest
6+
strategy:
7+
matrix:
8+
python-version: [3.6, 3.7, 3.8, 3.9]
9+
steps:
10+
- uses: actions/checkout@v2
11+
- name: Set up Python ${{ matrix.python-version }}
12+
uses: actions/setup-python@v2
13+
with:
14+
python-version: ${{ matrix.python-version }}
15+
- name: Install dependencies
16+
run: |
17+
python -m pip install --upgrade pip
18+
pip install -r requirements_test.txt
19+
- name: Test with phulp
20+
run: |
21+
phulp test

.gitignore

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
*.pyc
2+
*.pyo
3+
.cache
4+
.coverage
5+
.coverage.*
6+
.eggs
7+
.vscode
8+
coverage.xml
9+
venv/
10+
dist/
11+
build/
12+
pydantic_mongo.egg-info/
13+
var/
14+
.pytest_cache/

LICENSE.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
MIT License
2+
3+
Copyright (c) Jeferson <[email protected]> (https://jefersondaniel.com)
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6+
7+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
release:
2+
python setup.py sdist upload
3+
python setup.py bdist_wheel upload

README.rst

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
Pydantic Mongo
2+
======================================
3+
4+
|Build Status| |Version| |Pyversions|
5+
6+
Document object mapper for pydantic and pymongo
7+
8+
Documentation
9+
~~~~~~~~~~~~~
10+
11+
Usage
12+
^^^^^
13+
14+
Install:
15+
''''''''
16+
17+
.. code:: bash
18+
19+
$ pip install pydantic-mongo
20+
21+
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
22+
23+
.. code:: python
24+
25+
from pydantic import BaseModel
26+
from pydantic_mongo import AbstractRepository, ObjectIdField
27+
from pymongo import MongoClient
28+
29+
class Foo(BaseModel):
30+
count: int
31+
size: float = None
32+
33+
class Bar(BaseModel):
34+
apple = 'x'
35+
banana = 'y'
36+
37+
class Spam(BaseModel):
38+
id: ObjectIdField = None
39+
foo: Foo
40+
bars: List[Bar]
41+
42+
class Config:
43+
# The ObjectIdField creates an bson ObjectId value, so its necessary to setup the json encoding
44+
json_encoders = {ObjectId: str}
45+
46+
class SpamRepository(AbstractRepository[Spam]):
47+
class Meta:
48+
collection_name = 'spams'
49+
50+
client = MongoClient(os.environ["MONGODB_URL"])
51+
database = client[os.environ["MONGODB_DATABASE"]]
52+
53+
spam = Spam(foo=Foo(count=1, size=1.0),bars=[Bar()])
54+
55+
spam_repository = SpamRepository(database=database)
56+
57+
# Insert / Update
58+
spam_repository.save(spam)
59+
60+
# Delete
61+
spam_repository.delete(spam)
62+
63+
# Find One By Id
64+
result = spam_repository.find_one_by_id(spam.id)
65+
66+
# Find One By Query
67+
result = spam_repository.find_one_by({'foo.count': 1})
68+
69+
# Find By Query
70+
results = spam_repository.find_by({'foo.count': {'$gte': 1}})
71+
72+
''''
73+
74+
.. |Build Status| image:: https://travis-ci.org/jefersondaniel/pydantic-mongo.svg
75+
:target: https://travis-ci.org/jefersondaniel/pydantic-mongo
76+
77+
.. |Version| image:: https://badge.fury.io/py/pydantic-mongo.svg
78+
:target: https://pypi.python.org/pypi/pydantic-mongo
79+
80+
.. |Pyversions| image:: https://img.shields.io/pypi/pyversions/pydantic-mongo.svg
81+
:target: https://pypi.python.org/pypi/pydantic-mongo

phulpyfile.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import xml.etree.ElementTree as ET
2+
from os import system, unlink
3+
from os.path import dirname, join
4+
from phulpy import task
5+
6+
7+
@task
8+
def test(phulpy):
9+
phulpy.start(['lint', 'typecheck', 'unit_test'])
10+
11+
12+
@task
13+
def lint(phulpy):
14+
result = system('flake8 pydantic_mongo')
15+
if result:
16+
raise Exception('lint test failed')
17+
18+
19+
@task
20+
def unit_test(phulpy):
21+
result = system(
22+
'pytest --cov-report term-missing'
23+
+ ' --cov-report xml --cov=pydantic_mongo test'
24+
)
25+
if result:
26+
raise Exception('Unit tests failed')
27+
coverage_path = join(dirname(__file__), 'coverage.xml')
28+
xml = ET.parse(coverage_path).getroot()
29+
unlink(coverage_path)
30+
if float(xml.get('line-rate')) < 1:
31+
raise Exception('Unit test is not fully covered')
32+
33+
34+
@task
35+
def typecheck(phulpy):
36+
result = system(r'find ./pydantic_mongo -name "*.py" -exec mypy --ignore-missing-imports --follow-imports=skip --strict-optional {} \+')
37+
if result:
38+
raise Exception('lint test failed')

pydantic_mongo/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .abstract_repository import AbstractRepository # noqa
2+
from .fields import ObjectIdField # noqa

pydantic_mongo/abstract_repository.py

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from typing import Optional, TypeVar, Generic
2+
from pydantic import BaseModel
3+
4+
T = TypeVar('T', bound=BaseModel)
5+
6+
7+
class AbstractRepository(Generic[T]):
8+
def __init__(self, database):
9+
super().__init__()
10+
self.__database = database
11+
self.__document_class = self.__orig_bases__[0].__args__[0]
12+
self.__collection_name = self.Meta.collection_name
13+
self.__validate()
14+
15+
def get_collection(self):
16+
return self.__database[self.__collection_name]
17+
18+
def __validate(self):
19+
if not issubclass(self.__document_class, BaseModel):
20+
raise Exception('Document class should inherit BaseModel')
21+
if 'id' not in self.__document_class.__fields__:
22+
raise Exception('Document class should have id field')
23+
if not self.__collection_name:
24+
raise Exception('Meta should contain collection name')
25+
26+
def to_document(self, model: T) -> dict:
27+
result = model.dict()
28+
result.pop('id')
29+
if model.id:
30+
result['_id'] = model.id
31+
return result
32+
33+
def __to_query(self, data: dict):
34+
query = data.copy()
35+
if 'id' in data:
36+
query['_id'] = query.pop('id')
37+
return query
38+
39+
def to_model(self, data: dict) -> T:
40+
data_copy = data.copy()
41+
if '_id' in data_copy:
42+
data_copy['id'] = data_copy.pop('_id')
43+
return self.__document_class.parse_obj(data_copy)
44+
45+
def save(self, model: T):
46+
document = self.to_document(model)
47+
48+
if model.id:
49+
mongo_id = document.pop('_id')
50+
self.get_collection().update_one({'_id': mongo_id}, {'$set': document})
51+
return
52+
53+
result = self.get_collection().insert_one(document)
54+
model.id = result.inserted_id
55+
return result
56+
57+
def delete(self, model: T):
58+
return self.get_collection().delete_one({'_id': model.id})
59+
60+
def find_one_by_id(self, id: str) -> Optional[T]:
61+
return self.find_one_by({'id': id})
62+
63+
def find_one_by(self, query: dict) -> Optional[T]:
64+
result = self.get_collection().find_one(self.__to_query(query))
65+
return self.to_model(result) if result else None
66+
67+
def find_by(
68+
self,
69+
query: dict,
70+
skip: Optional[int] = None,
71+
limit: Optional[int] = None,
72+
sort=None
73+
):
74+
cursor = self.get_collection().find(self.__to_query(query))
75+
if limit:
76+
cursor.limit(limit)
77+
if skip:
78+
cursor.skip(skip)
79+
if sort:
80+
cursor.sort(sort)
81+
return map(self.to_model, cursor)

pydantic_mongo/fields.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from bson import ObjectId
2+
3+
4+
class ObjectIdField():
5+
@classmethod
6+
def __get_validators__(cls):
7+
yield cls.validate
8+
9+
@classmethod
10+
def validate(cls, value):
11+
if not ObjectId.is_valid(value):
12+
raise ValueError("Invalid id")
13+
14+
return ObjectId(value)
15+
16+
@classmethod
17+
def __modify_schema__(cls, field_schema):
18+
field_schema.update(type="string")

pyproject.toml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[build-system]
2+
requires = [
3+
"setuptools>=42",
4+
"wheel"
5+
]
6+
build-backend = "setuptools.build_meta"

requirements_test.txt

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
mock
2+
coverage==5.2.1
3+
flake8==3.8.3
4+
phulpy==1.0.10
5+
pytest==5.4.3
6+
pytest-cov==2.10.0
7+
pytest-mock==3.2.0
8+
mongomock==3.23.0
9+
pydantic==1.8.2
10+
pymongo==3.12.0
11+
mypy==0.910
12+
mypy-extensions==0.4.3

setup.cfg

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[bdist_wheel]
2+
universal = 1

setup.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from setuptools import setup
2+
3+
long_description = open('README.rst', 'r').read()
4+
5+
setup(
6+
name='pydantic-mongo',
7+
version='0.0.1',
8+
packages=['pydantic_mongo'],
9+
setup_requires=['wheel'],
10+
install_requires=[
11+
'pymongo>=3.12,<4.0',
12+
'pydantic>=1.6.2,<2.0.0'
13+
],
14+
entry_points={
15+
"console_scripts": [
16+
"pydantic_mongo = pydantic_mongo.__main__:__main__"
17+
],
18+
},
19+
description="Document object mapper for pydantic and pymongo",
20+
long_description=long_description,
21+
url='https://github.com/jefersondaniel/pydantic-mongo',
22+
author='Jeferson Daniel',
23+
author_email='[email protected]',
24+
license='MIT',
25+
classifiers=[
26+
'License :: OSI Approved :: MIT License',
27+
'Operating System :: OS Independent',
28+
'Programming Language :: Python :: 3',
29+
],
30+
python_requires=">=3.6"
31+
)

test/__init__.py

Whitespace-only changes.

test/test_fields.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import pytest
2+
from bson import ObjectId
3+
from pydantic_mongo import ObjectIdField
4+
from pydantic import BaseModel
5+
from pydantic.error_wrappers import ValidationError
6+
7+
8+
class User(BaseModel):
9+
id: ObjectIdField = None
10+
11+
class Config:
12+
json_encoders = {ObjectId: str}
13+
14+
15+
class TestFields:
16+
def test_object_id_validation(self):
17+
with pytest.raises(ValidationError):
18+
User.parse_obj({'id': 'lala'})
19+
User.parse_obj({'id': '611827f2878b88b49ebb69fc'})
20+
21+
def test_object_id_serialize(self):
22+
lala = User(id=ObjectId('611827f2878b88b49ebb69fc'))
23+
json_result = lala.json()
24+
assert '{"id": "611827f2878b88b49ebb69fc"}' == json_result
25+
26+
def test_modify_schema(self):
27+
user = User(id=ObjectId('611827f2878b88b49ebb69fc'))
28+
schema = user.schema()
29+
assert {
30+
'title': 'User',
31+
'type': 'object',
32+
'properties': {
33+
'id': {
34+
'title': 'Id',
35+
'type': 'string'
36+
}
37+
}
38+
} == schema

0 commit comments

Comments
 (0)