Skip to content

Commit db5a3e7

Browse files
Add simple but funcitonal example app.
1 parent 09056f5 commit db5a3e7

18 files changed

+929
-0
lines changed

example/.mypy.ini

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[mypy]
2+
3+
;
4+
; Import discovery
5+
;
6+
; tell mypy where the project root is for module resolution
7+
mypy_path=$PYTHONPATH,$PYTHONPATH/stubs
8+
; tell mypy to look at namespace packages (ones without an __init__.py)
9+
namespace_packages = True
10+
11+
;
12+
; Strict mode, almost
13+
;
14+
disallow_any_generics = True
15+
disallow_subclassing_any = True
16+
disallow_untyped_calls = True
17+
disallow_untyped_defs = True
18+
disallow_incomplete_defs = True
19+
check_untyped_defs = True
20+
disallow_untyped_decorators = True
21+
no_implicit_optional = True
22+
warn_redundant_casts = True
23+
warn_unused_ignores = True
24+
warn_return_any = True
25+
warn_unreachable = True
26+
strict_equality = True

example/.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.9.1

example/Dockerfile

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
FROM python:3.9.1-alpine3.12
2+
3+
# load source code
4+
RUN mkdir /app
5+
COPY example /app
6+
COPY requirements/prod.txt /app/requirements.txt
7+
COPY requirements/db_wrapper-0.1.0a0.tar.gz /app/requirements/db_wrapper-0.1.0a0.tar.gz
8+
9+
VOLUME /app
10+
WORKDIR /app
11+
12+
# install python dependencies
13+
RUN apk add --no-cache --virtual .build-deps \
14+
# needed to build psycopg2 & yarl
15+
gcc \
16+
# needed to build yarl
17+
musl-dev \
18+
# needed to build psycopg2
19+
postgresql-dev \
20+
# runtime dependency for psycopg2
21+
&& apk add --no-cache libpq \
22+
# install python packages
23+
&& ls -la requirements \
24+
&& pip install -r requirements.txt \
25+
# then remove build dependencies
26+
&& apk del .build-deps
27+
28+
# start server
29+
ENTRYPOINT ["python"]
30+
CMD ["example.py"]

example/docker-compose.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
version: "3" # this is the Docker Compose specification
2+
# version, not the app stack version
3+
4+
services:
5+
6+
# seed:
7+
# build: .
8+
# environment:
9+
# MODE: development
10+
# DB_HOST: db
11+
# DB_USER: test
12+
# DB_PASS: pass
13+
# DB_NAME: dev
14+
# volumes:
15+
# - ./example:/app
16+
17+
db:
18+
image: postgres:13-alpine
19+
restart: always
20+
ports:
21+
- 5432:5432
22+
environment:
23+
POSTGRES_DB: dev
24+
POSTGRES_USER: test
25+
POSTGRES_PASSWORD: pass # DONT DO THIS IN PROD -- acceptable in dev,
26+
# but docker secrets should be used in prod
27+
# in conjunction with docker swarm in place
28+
# of docker compose

example/example/example.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""An example of how to use Client & Model together."""
2+
3+
import asyncio
4+
import json
5+
import os
6+
from uuid import uuid4, UUID
7+
from typing import Any, List
8+
9+
from db_wrapper import ConnectionParameters, Client, Model
10+
11+
from models import AModel, ExtendedModel, ExtendedModelData
12+
13+
14+
class UUIDJsonEncoder(json.JSONEncoder):
15+
"""Extended Json Encoder to allow encoding of objects containing UUID."""
16+
17+
def default(self, obj: Any) -> Any:
18+
if isinstance(obj, UUID):
19+
return str(obj)
20+
21+
return obj
22+
23+
24+
conn_params = ConnectionParameters(
25+
host=os.getenv('DB_HOST', 'localhost'),
26+
# user=os.getenv('DB_USER', 'postgres'),
27+
# password=os.getenv('DB_PASS', 'postgres'),
28+
# database=os.getenv('DB_NAME', 'postgres'))
29+
user=os.getenv('DB_USER', 'test'),
30+
password=os.getenv('DB_PASS', 'pass'),
31+
database=os.getenv('DB_NAME', 'dev'))
32+
client = Client(conn_params)
33+
34+
a_model = Model[AModel](client, 'a_model')
35+
extended_model = ExtendedModel(client)
36+
37+
38+
async def create_a_model_record() -> UUID:
39+
"""
40+
Show how to use a simple Model instance.
41+
42+
Create a new record using the default Model.create.one method.
43+
"""
44+
new_record: AModel = {
45+
'_id': uuid4(),
46+
'string': 'some string',
47+
'integer': 1,
48+
'array': ['an', 'array', 'of', 'strings'],
49+
}
50+
51+
await a_model.create.one(new_record)
52+
53+
return new_record['_id']
54+
55+
56+
async def read_a_model(id_value: UUID) -> AModel:
57+
"""Show how to read a record with a given id value."""
58+
# read.one_by_id expects a string, so UUID values need
59+
# converted using str()
60+
return await a_model.read.one_by_id(str(id_value))
61+
62+
63+
async def create_extended_models() -> None:
64+
"""Show how using an extended Model can be the same as the defaults."""
65+
new_records: List[ExtendedModelData] = [{
66+
'_id': uuid4(),
67+
'string': 'something',
68+
'integer': 1,
69+
'json': {'a': 1, 'b': 2, 'c': True}
70+
}, {
71+
'_id': uuid4(),
72+
'string': 'something',
73+
'integer': 1,
74+
'json': {'a': 1, 'b': 2, 'c': True}
75+
}, {
76+
'_id': uuid4(),
77+
'string': 'something',
78+
'integer': 1,
79+
'json': {'a': 1, 'b': 2, 'c': True}
80+
}, {
81+
'_id': uuid4(),
82+
'string': 'something',
83+
'integer': 1,
84+
'json': {'a': 1, 'b': 2, 'c': True}
85+
}]
86+
87+
# by looping over a list of records, you can use the default create.one
88+
# method to create each record as a separate transaction
89+
for record in new_records:
90+
await extended_model.create.one(record)
91+
92+
93+
async def read_extended_models() -> List[ExtendedModelData]:
94+
"""Show how to use an extended Model's new methods."""
95+
# We defined read.all in ./models/extended_model.py's ExtendedRead class,
96+
# then replaced ExtendedModel's read property with ExtendedRead.
97+
# As a result, we can call it just like any other method on Model.read.
98+
return await extended_model.read.all()
99+
100+
101+
async def run() -> None:
102+
"""Show how to make a connection, execute queries, & disconnect."""
103+
await client.connect()
104+
105+
try:
106+
new_id = await create_a_model_record()
107+
created_a_model = await read_a_model(new_id)
108+
await create_extended_models()
109+
extended_models = await read_extended_models()
110+
finally:
111+
await client.disconnect()
112+
113+
print(json.dumps(created_a_model, cls=UUIDJsonEncoder))
114+
print(json.dumps(extended_models, cls=UUIDJsonEncoder))
115+
116+
if __name__ == '__main__':
117+
# A simple app can be run using asyncio's run method.
118+
# Sometimes, you may need more advanced loop management; look into
119+
# asyncio.get_event_loop for more.
120+
asyncio.run(run())

example/example/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .a_model import AModel
2+
from .extended_model import ExtendedModelData, ExtendedModel

example/example/models/a_model.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""Define a simple Model data type."""
2+
3+
from typing import List
4+
5+
from db_wrapper.model import ModelData
6+
7+
8+
class AModel(ModelData):
9+
"""An example Item."""
10+
11+
# PENDS python 3.9 support in pylint,
12+
# ModelData inherits from TypedDict
13+
# pylint: disable=too-few-public-methods
14+
15+
string: str
16+
integer: int
17+
array: List[str]

example/example/models/a_model.sql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
CREATE TABLE IF NOT EXISTS "a_model" (
2+
"_id" uuid PRIMARY KEY,
3+
"string" varchar(255),
4+
"integer" smallint,
5+
"array" varchar(255) []
6+
);
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""An example implementation of custom object Model."""
2+
3+
import json
4+
from typing import Any, List, Dict
5+
6+
from psycopg2 import sql
7+
from psycopg2.extensions import register_adapter
8+
from psycopg2.extras import Json
9+
10+
from db_wrapper.model import ModelData, Model, Read, Create, Client
11+
12+
# tell psycopg2 to adapt all dictionaries to json instead of
13+
# the default hstore
14+
register_adapter(dict, Json)
15+
16+
17+
class ExtendedModelData(ModelData):
18+
"""An example Item."""
19+
20+
# PENDS python 3.9 support in pylint,
21+
# ModelData inherits from TypedDict
22+
# pylint: disable=too-few-public-methods
23+
24+
string: str
25+
integer: int
26+
json: Dict[str, Any]
27+
28+
29+
class ExtendedCreator(Create[ExtendedModelData]):
30+
"""Add custom json loading to Model.create."""
31+
32+
# pylint: disable=too-few-public-methods
33+
34+
async def one(self, item: ExtendedModelData) -> ExtendedModelData:
35+
"""Override default Model.create.one method."""
36+
columns: List[sql.Identifier] = []
37+
values: List[sql.Identifier] = []
38+
39+
for column, value in item.items():
40+
if column == 'json':
41+
values.append(sql.Literal(json.dumps(value)))
42+
else:
43+
values.append(sql.Literal(value))
44+
45+
columns.append(sql.Identifier(column))
46+
47+
query = sql.SQL(
48+
'INSERT INTO {table} ({columns}) '
49+
'VALUES ({values}) '
50+
'RETURNING *;'
51+
).format(
52+
table=self._table,
53+
columns=sql.SQL(',').join(columns),
54+
values=sql.SQL(',').join(values),
55+
)
56+
57+
result: List[ExtendedModelData] = \
58+
await self._client.execute_and_return(query)
59+
60+
return result[0]
61+
62+
63+
class ExtendedReader(Read[ExtendedModelData]):
64+
"""Add custom method to Model.read."""
65+
66+
async def all_by_string(self, string: str) -> List[ExtendedModelData]:
67+
"""Read all rows with matching `string` value."""
68+
query = sql.SQL(
69+
'SELECT * '
70+
'FROM {table} '
71+
'WHERE string = {string};'
72+
).format(
73+
table=self._table,
74+
string=sql.Identifier(string)
75+
)
76+
77+
result: List[ExtendedModelData] = await self \
78+
._client.execute_and_return(query)
79+
80+
return result
81+
82+
async def all(self) -> List[ExtendedModelData]:
83+
"""Read all rows."""
84+
query = sql.SQL('SELECT * FROM {table}').format(
85+
table=self._table)
86+
87+
result: List[ExtendedModelData] = await self \
88+
._client.execute_and_return(query)
89+
90+
return result
91+
92+
93+
class ExtendedModel(Model[ExtendedModelData]):
94+
"""Build an ExampleItem Model instance."""
95+
96+
read: ExtendedReader
97+
create: ExtendedCreator
98+
99+
def __init__(self, client: Client) -> None:
100+
super().__init__(client, 'extended_model')
101+
self.read = ExtendedReader(self.client, self.table)
102+
self.create = ExtendedCreator(self.client, self.table)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
CREATE EXTENSION hstore;
2+
3+
CREATE TABLE IF NOT EXISTS "extended_model" (
4+
"_id" uuid PRIMARY KEY,
5+
"string" varchar(255),
6+
"integer" smallint,
7+
"json" jsonb
8+
);

example/example/py.typed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Marker

0 commit comments

Comments
 (0)