You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+92-46
Original file line number
Diff line number
Diff line change
@@ -6,7 +6,7 @@ This lab aims to outline a recipe for building a standardised Python server that
6
6
-[X] A Redis server for development
7
7
-[ ] Healthcheck endpoint that will validate that the API can get to the database
8
8
-[X] Worker processes that will process tasks in the background (using Celery)
9
-
-[] Provide `Dockerfile` for development and production
9
+
-[X] Provide `Dockerfile` for development and production
10
10
-[ ] Log aggregation and monitoring ([parseable](https://github.com/parseablehq/parseable))
11
11
-[X]~~CSRF protection~~ see [#52](https://github.com/anomaly/lab-python-server/issues/52), also see [official guide](https://fastapi.tiangolo.com/tutorial/cors/)
12
12
-[X] Basic endpoints for authentication (JWT and OTP based) - along with recommendations for encryption algorithms
@@ -55,15 +55,26 @@ The above is wrapped up as a `Task` endpoints, you need to supply the length of
55
55
task crypt:hash -- 32
56
56
```
57
57
58
+
## Exposed ports for development
59
+
60
+
If you are using the development `docker-compose.yml` it exposes the following ports to the host machine:
61
+
62
+
-`5432` - standard port for `postgres` so you can use a developer tool to inspect the database
63
+
-`15672` - RabbitMQ web dashboard (HTTP)
64
+
-`9000` - MinIO web server was exchanging S3 compatible objects (HTTPS, see configuration details)
65
+
-`9001` - MinIO web Dashboard (HTTPS)
66
+
67
+
> Some of these ports should not be exposed in production
68
+
58
69
## Python packages
59
70
60
71
The following Python packages make the standard set of tools for our projects:
61
72
62
73
-[**SQLAlchemy**](https://www.sqlalchemy.org) - A Python object relational mapper (ORM)
63
74
-[**alembic**](https://alembic.sqlalchemy.org/en/latest/) - A database migration tool
64
75
-[**FastAPI**](http://fastapi.tiangolo.com) - A fast, simple, and flexible framework for building HTTP APIs
65
-
-[**Celery**](https://docs.celeryq.dev/en/stable/getting-started/introduction.html) - A task queue
66
-
-**fluent-logger** - A Python logging library that supports fluentd
76
+
-[**pydantic**](https://docs.pydantic.dev) - A data validation library that is central around the design of FastAPI
77
+
-[**TaskIQ**](https://https://taskiq-python.github.io/) - An `asyncio` compatible task queue processor that uses RabbitMQ and Redis and has FastAPI like design e.g Dependencies
67
78
-[**pendulum**](https://pendulum.eustace.io) - A timezone aware datetime library
68
79
-[**pyotp**](https://pyauth.github.io/pyotp/) - A One-Time Password (OTP) generator
69
80
@@ -98,13 +109,13 @@ Directory structure for our application:
98
109
├─ tests/
99
110
├─ labs
100
111
| └─ routers/ -- FastAPI routers
101
-
| └─ tasks/ -- Celery tasks
112
+
| └─ tasks/ -- TaskIQ
102
113
| └─ models/ -- SQLAlchemy models
103
114
| └─ schema/ -- Pydantic schemas
104
115
| └─ alembic/ -- Alembic migrations
105
116
| └─ __init__.py
106
117
| └─ api.py
107
-
| └─ celery.py
118
+
| └─ broker.py
108
119
| └─ config.py
109
120
| └─ db.py
110
121
| └─ utils.py
@@ -248,64 +259,62 @@ which would result in the client generating a function like `someSpecificIdYouDe
248
259
249
260
For consistenty FastAPI docs shows a wrapper function that [globally re-writes](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/?h=operation_id#using-the-path-operation-function-name-as-the-operationid) the `operation_id` to the function name. This does put the onus on the developer to name the function correctly.
250
261
251
-
## Celery based workers
262
+
## TaskIQ based tasks
252
263
253
-
> *WARNING:* Celery currently *DOES NOT* have support for `asyncio`which comes in the way of our stack, please follow [Issue 21](https://github.com/anomaly/lab-python-server/issues/21) for information on current work arounds and recommendations. We are also actively working with the Celery team to get this resolved.
264
+
The project uses [`TaskIQ`](https://taskiq-python.github.io) to manage task queues. TaskIQ supports `asyncio`and has FastAPI like design ideas e.g [dependency injection](https://taskiq-python.github.io/guide/state-and-deps.html)and can be tightly [coupled with FastAPI](https://taskiq-python.github.io/guide/taskiq-with-fastapi.html).
254
265
255
-
The projects use `Celery` to manage a queue backed by `redis` to schedule and process background tasks. The celery app is run a separate container. In development we use [watchdog](https://github.com/gorakhargosh/watchdog) to watch for changes to the Python files, this is obviously uncessary in production.
256
266
257
-
The celery app is configured in `celery.py` which reads from the `redis` configuration in `config.py`.
267
+
TaskIQ is configured as recommend for production use with [taskiq-aio-pika](https://pypi.org/project/taskiq-aio-pika/) as the broker and [taskiq-redis](https://pypi.org/project/taskiq-redis/) as the result backend.
258
268
259
-
Each task is defined in the `tasks` package with appropriate subpackages.
269
+
`broker.py` in the root of the project configures the broker using:
260
270
261
-
To schedule tasks, the API endpoints need to import the task
262
271
```python
263
-
from ...tasks.email import verification_email
272
+
broker = AioPikaBroker(
273
+
config.amqp_dsn,
274
+
result_backend=redis_result_backend
275
+
)
264
276
```
265
-
and call the `apply_async` method on the task:
277
+
278
+
`api.py` uses `FastAPI` events to `start` and `shutdown` the broker. As their documentation notes:
279
+
280
+
> Calling the startup method is necessary. If you don't call it, you may get an undefined behaviour.
281
+
266
282
```python
267
-
verification_email.apply_async()
283
+
# TaskIQ configurartion so we can share FastAPI dependencies in tasks
284
+
@app.on_event("startup")
285
+
asyncdefapp_startup():
286
+
ifnot broker.is_worker_process:
287
+
await broker.startup()
288
+
289
+
# On shutdown, we need to shutdown the broker
290
+
@app.on_event("shutdown")
291
+
asyncdefapp_shutdown():
292
+
ifnot broker.is_worker_process:
293
+
await broker.shutdown()
268
294
```
269
295
270
-
A pieced together example of scheduling a task:
296
+
We recommend creating a `tasks.py` file under each router directory to keep the tasks associated to each router group next to them. Tasks can be defined by simply calling the `task` decorator on the `broker`:
271
297
272
298
```python
273
-
from fastapi import APIRouter, Request, Depends
274
-
from sqlalchemy.ext.asyncio import AsyncSession
275
-
276
-
from ...db import session_context, session_context
logging.error("Kicking off send_account_verification_email")
303
+
```
279
304
280
-
router = APIRouter()
305
+
and kick it off simply use the `kiq` method from the FastAPI handlers:
281
306
307
+
```python
282
308
@router.get("/verify")
283
-
asyncdeflog(request: Request):
309
+
asyncdefverify_user(request: Request):
284
310
"""Verify an account
285
311
"""
286
-
verification_email.apply_async()
312
+
await send_account_verification_email.kiq()
287
313
return {"message": "hello world"}
288
314
```
289
315
290
-
You can send position arguments to the task, for example:
291
-
292
-
```python
293
-
verification_email.apply_async(args=[user_id])
294
-
```
295
-
296
-
which would be recieved by the task as `user_id` as a positional argument.
297
-
298
-
> We recommend reading design documentation for the `Celery` project [here](https://docs.celeryproject.org/en/latest/userguide/tasks.html), the general principle is send meta data that the task can use to complete the task not complete, heavy objects. i.e send an ID with some context as opposed to a fully formed object.
299
-
300
-
### Monitoring the queue
316
+
There are various powerful options for queuing tasks both scheduled and periodic tasks are supported.
301
317
302
-
Celery can be monitored using the `flower` package, it provides a web based interfaces. There's also a text based interface available via the command line interface:
303
-
304
-
```sh
305
-
celery -A labs.celery:app events
306
-
```
307
-
308
-
you can alternatively use the wrapped Task command `task dev:qwatch`, this is portable across projects if you copy the template.
309
318
310
319
## SQLAlchemy wisdom
311
320
@@ -487,15 +496,52 @@ INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
487
496
INFO [alembic.runtime.migration] Will assume transactional DDL.
488
497
INFO [alembic.runtime.migration] Running upgrade -> 4b2dfa16da8f, init db
489
498
```
499
+
### Joining back with `HEAD`
500
+
501
+
`task db:heads`
502
+
503
+
## MinIO wisdom
504
+
505
+
MinIO is able to run with `TLS` enabled, all you hve to do is provide it a certificate. By default MinIO looks for certificates in `${HOME}/.minio/certs`. You can generate certificates and mount them into the container:
506
+
507
+
```yaml
508
+
volumes:
509
+
- minio-data:/data
510
+
- .cert:/root/.minio/certs
511
+
```
512
+
513
+
This will result in the dashboard being available via `HTTPS` and the signed URLs will be TLS enabled.
514
+
515
+
Since we use `TLS` enabled endpoints for development, running MinIO in secure mode will satisfy any browser security policies.
516
+
517
+
### S3FileMetadata
518
+
519
+
The template provides a `SQLAlchemy` table called `S3FileMetadata` this is used to store metadata about file uploads.
520
+
521
+
The client sends a request with the file `name`, `size` and `mime type`, the endpoint create a `S3FileMetadata` and returns an pre-signed upload URL, that the client must post the contents of the file to.
522
+
523
+
The client can take as long as it takes it upload the contents, but must begin uploading within the signed life e.g five minutes from when the URL is generated.
524
+
525
+
The template is designed to schedule a task to check if the object made it to the store. It continues to check this for a period of time and marks the file to be available if the contents are found on the object store.
526
+
527
+
The client must keep polling back to the server to see if the file is eventually available.
490
528
491
529
## Taskfile
492
530
493
531
[Task](https://taskfile.dev) is a task runner / build tool that aims to be simpler and easier to use than, for example, GNU Make. Wile it's useful to know the actual commands it's easy to use a tool like task to make things easier on a daily basis:
494
532
495
-
-`task db:revision -- "commit message"` - creates a new revision in the database and uses the parameter as the commit message
496
-
-`task db:migrate` - migrates the schema to the latest version
497
-
-`task dev:psql` - postgres shell in the database container
498
-
-`task dev:pyshell` - get a `python` session on the api container which should have access to the entire application
533
+
- `eject`- eject the project from a template
534
+
- `build:image`- builds a publishable docker image
535
+
- `crypt:hash`- generate a random cryptographic hash
536
+
- `db:alembic`- arbitrary alembic command in the container
537
+
- `db:heads`- shows the HEAD SHA for alembic migrations
538
+
- `db:init`- initialise the database schema
539
+
- `db:migrate`- migrates models to HEAD
540
+
- `db:rev`- create a database migration, pass a string as commit string
541
+
- `dev:psql`- postgres shell on the db container
542
+
- `dev:pyshell`- get a python session on the api container
543
+
- `dev:sh`- get a bash session on the api container
544
+
- `dev:test`- runs tests inside the server container
0 commit comments