From 73c859f2d1e302c8d62b520e5a58ae73da77a8e0 Mon Sep 17 00:00:00 2001 From: juftin Date: Sun, 20 Aug 2023 21:11:39 -0600 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20SQLAlchemy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yaml | 1 + docs/infrastructure.md | 15 - docs/openapi.json | 1357 +++++++---------- migrations/env.py | 9 +- migrations/script.py.mako | 1 - ..._13_0009-9c5764f1931c_initial_migration.py | 107 -- .../2023_08_13_0025-5ff3a5bf3a14_seed_data.py | 123 -- ...3_08_17_1904-47783d8459db_fastapi_users.py | 76 - ..._20_1729-e5fccc3522ce_initial_migration.py | 157 ++ .../2023_08_20_1730-e69c9264375d_seed_data.py | 114 ++ pyproject.toml | 27 +- requirements.txt | 284 ++-- tests/backend/test_animals.py | 6 +- tests/models/test_animals_models.py | 2 +- zoo/__main__.py | 65 + zoo/{backend => api}/__init__.py | 0 zoo/{backend => api}/animals.py | 45 +- zoo/{backend => api}/exhibits.py | 63 +- zoo/{backend => api}/staff.py | 45 +- zoo/{backend => api}/utils.py | 16 +- zoo/app.py | 49 +- zoo/config.py | 34 +- zoo/db.py | 10 +- zoo/models/__init__.py | 2 +- zoo/models/animals.py | 99 +- zoo/models/base.py | 133 +- zoo/models/exhibits.py | 94 +- zoo/models/staff.py | 115 +- zoo/models/{user.py => users.py} | 98 +- zoo/schemas/__init__.py | 0 zoo/schemas/animals.py | 70 + zoo/schemas/base.py | 119 ++ zoo/schemas/exhibits.py | 65 + zoo/schemas/staff.py | 78 + zoo/schemas/users.py | 30 + zoo/{models => schemas}/utils.py | 16 +- 36 files changed, 1683 insertions(+), 1842 deletions(-) delete mode 100644 migrations/versions/2023_08_13_0009-9c5764f1931c_initial_migration.py delete mode 100644 migrations/versions/2023_08_13_0025-5ff3a5bf3a14_seed_data.py delete mode 100644 migrations/versions/2023_08_17_1904-47783d8459db_fastapi_users.py create mode 100644 migrations/versions/2023_08_20_1729-e5fccc3522ce_initial_migration.py create mode 100644 migrations/versions/2023_08_20_1730-e69c9264375d_seed_data.py create mode 100644 zoo/__main__.py rename zoo/{backend => api}/__init__.py (100%) rename zoo/{backend => api}/animals.py (68%) rename zoo/{backend => api}/exhibits.py (71%) rename zoo/{backend => api}/staff.py (68%) rename zoo/{backend => api}/utils.py (83%) rename zoo/models/{user.py => users.py} (58%) create mode 100644 zoo/schemas/__init__.py create mode 100644 zoo/schemas/animals.py create mode 100644 zoo/schemas/base.py create mode 100644 zoo/schemas/exhibits.py create mode 100644 zoo/schemas/staff.py create mode 100644 zoo/schemas/users.py rename zoo/{models => schemas}/utils.py (72%) diff --git a/docker-compose.yaml b/docker-compose.yaml index d85d263..e67b158 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -13,6 +13,7 @@ services: environment: ZOO_PRODUCTION: true ZOO_DOCKER: true + ZOO_JWT_EXPIRATION: 3600 ZOO_DATABASE_DRIVER: postgresql+asyncpg ZOO_DATABASE_HOST: db ZOO_DATABASE_PORT: 5432 diff --git a/docs/infrastructure.md b/docs/infrastructure.md index 8f657e5..5a6fcef 100644 --- a/docs/infrastructure.md +++ b/docs/infrastructure.md @@ -27,21 +27,6 @@ interface for interacting with databases in Python. SQLModel is built on top of [Pydantic](https://pydantic-docs.helpmanual.io/) and [SQLAlchemy](https://www.sqlalchemy.org/), a popular SQL toolkit and ORM for Python. -#### SQLModel - -```python -from typing import Optional - -from sqlmodel import Field, SQLModel - - -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str - secret_name: str - age: Optional[int] = None -``` - ### Database Migrations `zoo` uses [Alembic](https://alembic.sqlalchemy.org/en/latest/) for database migrations. Alembic diff --git a/docs/openapi.json b/docs/openapi.json index c4aed34..c46e577 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -38,25 +38,25 @@ "operationId": "animals-get_animals", "parameters": [ { + "name": "offset", + "in": "query", "required": false, "schema": { "type": "integer", - "title": "Offset", - "default": 0 - }, - "name": "offset", - "in": "query" + "default": 0, + "title": "Offset" + } }, { + "name": "limit", + "in": "query", "required": false, "schema": { "type": "integer", - "maximum": 100.0, - "title": "Limit", - "default": 100 - }, - "name": "limit", - "in": "query" + "maximum": 100, + "default": 100, + "title": "Limit" + } } ], "responses": { @@ -65,10 +65,10 @@ "content": { "application/json": { "schema": { + "type": "array", "items": { "$ref": "#/components/schemas/AnimalsRead" }, - "type": "array", "title": "Response Animals-Get Animals" } } @@ -94,14 +94,14 @@ "description": "Create a new animal in the database", "operationId": "animals-create_animal", "requestBody": { + "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AnimalsCreate" } } - }, - "required": true + } }, "responses": { "200": { @@ -137,13 +137,13 @@ "operationId": "animals-get_animal", "parameters": [ { + "name": "animal_id", + "in": "path", "required": true, "schema": { "type": "integer", "title": "Animal Id" - }, - "name": "animal_id", - "in": "path" + } } ], "responses": { @@ -178,13 +178,13 @@ "operationId": "animals-delete_animal", "parameters": [ { + "name": "animal_id", + "in": "path", "required": true, "schema": { "type": "integer", "title": "Animal Id" - }, - "name": "animal_id", - "in": "path" + } } ], "responses": { @@ -219,24 +219,24 @@ "operationId": "animals-update_animal", "parameters": [ { + "name": "animal_id", + "in": "path", "required": true, "schema": { "type": "integer", "title": "Animal Id" - }, - "name": "animal_id", - "in": "path" + } } ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AnimalsUpdate" } } - }, - "required": true + } }, "responses": { "200": { @@ -272,25 +272,25 @@ "operationId": "exhibits-get_exhibits", "parameters": [ { + "name": "offset", + "in": "query", "required": false, "schema": { "type": "integer", - "title": "Offset", - "default": 0 - }, - "name": "offset", - "in": "query" + "default": 0, + "title": "Offset" + } }, { + "name": "limit", + "in": "query", "required": false, "schema": { "type": "integer", - "maximum": 100.0, - "title": "Limit", - "default": 100 - }, - "name": "limit", - "in": "query" + "maximum": 100, + "default": 100, + "title": "Limit" + } } ], "responses": { @@ -299,10 +299,10 @@ "content": { "application/json": { "schema": { + "type": "array", "items": { "$ref": "#/components/schemas/ExhibitsRead" }, - "type": "array", "title": "Response Exhibits-Get Exhibits" } } @@ -328,14 +328,14 @@ "description": "Create a new exhibit in the database", "operationId": "exhibits-create_exhibit", "requestBody": { + "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ExhibitsCreate" } } - }, - "required": true + } }, "responses": { "200": { @@ -371,13 +371,13 @@ "operationId": "exhibits-get_exhibit", "parameters": [ { + "name": "exhibit_id", + "in": "path", "required": true, "schema": { "type": "integer", "title": "Exhibit Id" - }, - "name": "exhibit_id", - "in": "path" + } } ], "responses": { @@ -412,13 +412,13 @@ "operationId": "exhibits-delete_exhibit", "parameters": [ { + "name": "exhibit_id", + "in": "path", "required": true, "schema": { "type": "integer", "title": "Exhibit Id" - }, - "name": "exhibit_id", - "in": "path" + } } ], "responses": { @@ -453,24 +453,24 @@ "operationId": "exhibits-update_exhibit", "parameters": [ { + "name": "exhibit_id", + "in": "path", "required": true, "schema": { "type": "integer", "title": "Exhibit Id" - }, - "name": "exhibit_id", - "in": "path" + } } ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ExhibitsUpdate" } } - }, - "required": true + } }, "responses": { "200": { @@ -506,13 +506,13 @@ "operationId": "exhibits-get_exhibit_animals", "parameters": [ { + "name": "exhibit_id", + "in": "path", "required": true, "schema": { "type": "integer", "title": "Exhibit Id" - }, - "name": "exhibit_id", - "in": "path" + } } ], "responses": { @@ -521,10 +521,10 @@ "content": { "application/json": { "schema": { + "type": "array", "items": { "$ref": "#/components/schemas/AnimalsRead" }, - "type": "array", "title": "Response Exhibits-Get Exhibit Animals" } } @@ -553,13 +553,13 @@ "operationId": "exhibits-get_exhibit_staff", "parameters": [ { + "name": "exhibit_id", + "in": "path", "required": true, "schema": { "type": "integer", "title": "Exhibit Id" - }, - "name": "exhibit_id", - "in": "path" + } } ], "responses": { @@ -568,10 +568,10 @@ "content": { "application/json": { "schema": { + "type": "array", "items": { "$ref": "#/components/schemas/StaffRead" }, - "type": "array", "title": "Response Exhibits-Get Exhibit Staff" } } @@ -600,25 +600,25 @@ "operationId": "staff-get_staff_members", "parameters": [ { + "name": "offset", + "in": "query", "required": false, "schema": { "type": "integer", - "title": "Offset", - "default": 0 - }, - "name": "offset", - "in": "query" + "default": 0, + "title": "Offset" + } }, { + "name": "limit", + "in": "query", "required": false, "schema": { "type": "integer", - "maximum": 100.0, - "title": "Limit", - "default": 100 - }, - "name": "limit", - "in": "query" + "maximum": 100, + "default": 100, + "title": "Limit" + } } ], "responses": { @@ -627,10 +627,10 @@ "content": { "application/json": { "schema": { + "type": "array", "items": { "$ref": "#/components/schemas/StaffRead" }, - "type": "array", "title": "Response Staff-Get Staff Members" } } @@ -656,14 +656,14 @@ "description": "Create a new staff in the database", "operationId": "staff-create_staff", "requestBody": { + "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StaffCreate" } } - }, - "required": true + } }, "responses": { "200": { @@ -699,13 +699,13 @@ "operationId": "staff-get_staff", "parameters": [ { + "name": "staff_id", + "in": "path", "required": true, "schema": { "type": "integer", "title": "Staff Id" - }, - "name": "staff_id", - "in": "path" + } } ], "responses": { @@ -740,13 +740,13 @@ "operationId": "staff-delete_staff", "parameters": [ { + "name": "staff_id", + "in": "path", "required": true, "schema": { "type": "integer", "title": "Staff Id" - }, - "name": "staff_id", - "in": "path" + } } ], "responses": { @@ -781,24 +781,24 @@ "operationId": "staff-update_staff", "parameters": [ { + "name": "staff_id", + "in": "path", "required": true, "schema": { "type": "integer", "title": "Staff Id" - }, - "name": "staff_id", - "in": "path" + } } ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StaffUpdate" } } - }, - "required": true + } }, "responses": { "200": { @@ -835,7 +835,7 @@ "content": { "application/x-www-form-urlencoded": { "schema": { - "$ref": "#/components/schemas/Body_auth-auth_jwt.login" + "$ref": "#/components/schemas/Body_auth-auth" } } }, @@ -987,578 +987,114 @@ } } } - }, - "/auth/forgot-password": { - "post": { - "tags": [ - "auth" - ], - "summary": "Reset:Forgot Password", - "operationId": "auth-reset:forgot_password", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_auth-reset_forgot_password" + } + }, + "components": { + "schemas": { + "AnimalsCreate": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the animal" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - } + ], + "title": "Description", + "description": "The description of the animal" }, - "required": true - }, - "responses": { - "202": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} + "species": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - } + ], + "title": "Species", + "description": "The species of the animal" }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "exhibit_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" } - } + ], + "title": "Exhibit Id", + "description": "The id of the exhibit" } - } - } - }, - "/auth/reset-password": { - "post": { - "tags": [ - "auth" + }, + "type": "object", + "required": [ + "name" ], - "summary": "Reset:Reset Password", - "operationId": "auth-reset:reset_password", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_auth-reset_reset_password" - } - } + "title": "AnimalsCreate", + "description": "\n Animals model: create\n ", + "examples": [ + { + "description": "Ferocious kitty", + "exhibit_id": 1, + "name": "Lion", + "species": "Panthera leo" + } + ] + }, + "AnimalsRead": { + "properties": { + "id": { + "type": "integer", + "title": "Id", + "description": "The unique identifier for the table" }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} + "name": { + "type": "string", + "title": "Name", + "description": "The name of the animal" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - } + ], + "title": "Description", + "description": "The description of the animal" }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - }, - "examples": { - "RESET_PASSWORD_BAD_TOKEN": { - "summary": "Bad or expired token.", - "value": { - "detail": "RESET_PASSWORD_BAD_TOKEN" - } - }, - "RESET_PASSWORD_INVALID_PASSWORD": { - "summary": "Password validation failed.", - "value": { - "detail": { - "code": "RESET_PASSWORD_INVALID_PASSWORD", - "reason": "Password should be at least 3 characters" - } - } - } - } + "species": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - } + ], + "title": "Species", + "description": "The species of the animal" }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "exhibit_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" } - } - } - } - } - }, - "/auth/request-verify-token": { - "post": { - "tags": [ - "auth" - ], - "summary": "Verify:Request-Token", - "operationId": "auth-verify:request-token", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_auth-verify_request-token" - } - } - }, - "required": true - }, - "responses": { - "202": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/auth/verify": { - "post": { - "tags": [ - "auth" - ], - "summary": "Verify:Verify", - "operationId": "auth-verify:verify", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_auth-verify_verify" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserRead" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - }, - "examples": { - "VERIFY_USER_BAD_TOKEN": { - "summary": "Bad token, not existing user ornot the e-mail currently set for the user.", - "value": { - "detail": "VERIFY_USER_BAD_TOKEN" - } - }, - "VERIFY_USER_ALREADY_VERIFIED": { - "summary": "The user is already verified.", - "value": { - "detail": "VERIFY_USER_ALREADY_VERIFIED" - } - } - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/users/me": { - "get": { - "tags": [ - "users" - ], - "summary": "Users:Current User", - "operationId": "users-users:current_user", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserRead" - } - } - } - }, - "401": { - "description": "Missing token or inactive user." - } - }, - "security": [ - { - "OAuth2PasswordBearer": [] - } - ] - }, - "patch": { - "tags": [ - "users" - ], - "summary": "Users:Patch Current User", - "operationId": "users-users:patch_current_user", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserUpdate" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserRead" - } - } - } - }, - "401": { - "description": "Missing token or inactive user." - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - }, - "examples": { - "UPDATE_USER_EMAIL_ALREADY_EXISTS": { - "summary": "A user with this email already exists.", - "value": { - "detail": "UPDATE_USER_EMAIL_ALREADY_EXISTS" - } - }, - "UPDATE_USER_INVALID_PASSWORD": { - "summary": "Password validation failed.", - "value": { - "detail": { - "code": "UPDATE_USER_INVALID_PASSWORD", - "reason": "Password should beat least 3 characters" - } - } - } - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - }, - "security": [ - { - "OAuth2PasswordBearer": [] - } - ] - } - }, - "/users/{id}": { - "get": { - "tags": [ - "users" - ], - "summary": "Users:User", - "operationId": "users-users:user", - "parameters": [ - { - "required": true, - "schema": { - "type": "string", - "title": "Id" - }, - "name": "id", - "in": "path" - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserRead" - } - } - } - }, - "401": { - "description": "Missing token or inactive user." - }, - "403": { - "description": "Not a superuser." - }, - "404": { - "description": "The user does not exist." - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - }, - "security": [ - { - "OAuth2PasswordBearer": [] - } - ] - }, - "delete": { - "tags": [ - "users" - ], - "summary": "Users:Delete User", - "operationId": "users-users:delete_user", - "parameters": [ - { - "required": true, - "schema": { - "type": "string", - "title": "Id" - }, - "name": "id", - "in": "path" - } - ], - "responses": { - "204": { - "description": "Successful Response" - }, - "401": { - "description": "Missing token or inactive user." - }, - "403": { - "description": "Not a superuser." - }, - "404": { - "description": "The user does not exist." - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - }, - "security": [ - { - "OAuth2PasswordBearer": [] - } - ] - }, - "patch": { - "tags": [ - "users" - ], - "summary": "Users:Patch User", - "operationId": "users-users:patch_user", - "parameters": [ - { - "required": true, - "schema": { - "type": "string", - "title": "Id" - }, - "name": "id", - "in": "path" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserUpdate" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserRead" - } - } - } - }, - "401": { - "description": "Missing token or inactive user." - }, - "403": { - "description": "Not a superuser." - }, - "404": { - "description": "The user does not exist." - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorModel" - }, - "examples": { - "UPDATE_USER_EMAIL_ALREADY_EXISTS": { - "summary": "A user with this email already exists.", - "value": { - "detail": "UPDATE_USER_EMAIL_ALREADY_EXISTS" - } - }, - "UPDATE_USER_INVALID_PASSWORD": { - "summary": "Password validation failed.", - "value": { - "detail": { - "code": "UPDATE_USER_INVALID_PASSWORD", - "reason": "Password should beat least 3 characters" - } - } - } - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - }, - "security": [ - { - "OAuth2PasswordBearer": [] - } - ] - } - } - }, - "components": { - "schemas": { - "AnimalsCreate": { - "properties": { - "name": { - "type": "string", - "title": "Name", - "description": "The name of the animal" - }, - "description": { - "type": "string", - "title": "Description", - "description": "The description of the animal" - }, - "species": { - "type": "string", - "title": "Species", - "description": "The species of the animal" - }, - "exhibit_id": { - "type": "integer", - "title": "Exhibit Id", - "description": "The id of the exhibit" - } - }, - "type": "object", - "required": [ - "name" - ], - "title": "AnimalsCreate", - "description": "Animals model: create", - "examples": [ - { - "name": "Lion", - "description": "Ferocious kitty", - "species": "Panthera leo", - "exhibit_id": 1 - } - ] - }, - "AnimalsRead": { - "properties": { - "id": { - "type": "integer", - "title": "Id", - "description": "The unique identifier for the table" - }, - "name": { - "type": "string", - "title": "Name", - "description": "The name of the animal" - }, - "description": { - "type": "string", - "title": "Description", - "description": "The description of the animal" - }, - "species": { - "type": "string", - "title": "Species", - "description": "The species of the animal" - }, - "exhibit_id": { - "type": "integer", + ], "title": "Exhibit Id", "description": "The id of the exhibit" }, @@ -1568,15 +1104,22 @@ "title": "Created At", "description": "The date and time the record was created" }, - "modified_at": { + "updated_at": { "type": "string", "format": "date-time", - "title": "Modified At", + "title": "Updated At", "description": "The date and time the record was last modified" }, "deleted_at": { - "type": "string", - "format": "date-time", + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], "title": "Deleted At", "description": "The date and time the record was deleted" } @@ -1584,52 +1127,82 @@ "type": "object", "required": [ "id", - "name" + "name", + "created_at", + "updated_at" ], "title": "AnimalsRead", - "description": "Animals model: read", + "description": "\n Animals model: read\n ", "examples": [ { - "name": "Lion", + "created_at": "2021-01-01T00:00:00.000000", "description": "Ferocious kitty", - "species": "Panthera leo", "exhibit_id": 1, "id": 1, - "created_at": "2021-01-01T00:00:00.000000", - "modified_at": "2021-01-02T09:12:34.567890" + "modified_at": "2021-01-02T09:12:34.567890", + "name": "Lion", + "species": "Panthera leo" } ] }, "AnimalsUpdate": { "properties": { "name": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Name", "description": "The name of the animal" }, "description": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Description", "description": "The description of the animal" }, "species": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Species", "description": "The species of the animal" }, "exhibit_id": { - "type": "integer", + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], "title": "Exhibit Id", "description": "The id of the exhibit" } }, "type": "object", "title": "AnimalsUpdate", - "description": "Animals model: update", + "description": "\n Animals model: update\n ", "examples": [ { - "name": "Lion", - "description": "Ferocious kitty" + "description": "Ferocious kitty", + "name": "Lion" } ] }, @@ -1651,11 +1224,18 @@ ], "title": "BearerResponse" }, - "Body_auth-auth_jwt.login": { + "Body_auth-auth": { "properties": { "grant_type": { - "type": "string", - "pattern": "password", + "anyOf": [ + { + "type": "string", + "pattern": "password" + }, + { + "type": "null" + } + ], "title": "Grant Type" }, "username": { @@ -1672,11 +1252,25 @@ "default": "" }, "client_id": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Client Id" }, "client_secret": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Client Secret" } }, @@ -1687,65 +1281,6 @@ ], "title": "Body_auth-auth:jwt.login" }, - "Body_auth-reset_forgot_password": { - "properties": { - "email": { - "type": "string", - "format": "email", - "title": "Email" - } - }, - "type": "object", - "required": [ - "email" - ], - "title": "Body_auth-reset:forgot_password" - }, - "Body_auth-reset_reset_password": { - "properties": { - "token": { - "type": "string", - "title": "Token" - }, - "password": { - "type": "string", - "title": "Password" - } - }, - "type": "object", - "required": [ - "token", - "password" - ], - "title": "Body_auth-reset:reset_password" - }, - "Body_auth-verify_request-token": { - "properties": { - "email": { - "type": "string", - "format": "email", - "title": "Email" - } - }, - "type": "object", - "required": [ - "email" - ], - "title": "Body_auth-verify:request-token" - }, - "Body_auth-verify_verify": { - "properties": { - "token": { - "type": "string", - "title": "Token" - } - }, - "type": "object", - "required": [ - "token" - ], - "title": "Body_auth-verify:verify" - }, "ErrorModel": { "properties": { "detail": { @@ -1777,12 +1312,26 @@ "description": "The name of the exhibit" }, "description": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Description", "description": "The description of the exhibit" }, "location": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Location", "description": "The location of the exhibit" } @@ -1792,12 +1341,12 @@ "name" ], "title": "ExhibitsCreate", - "description": "Exhibits model: create", + "description": "\n Exhibits model: create\n ", "examples": [ { - "name": "Big Cat Exhibit", "description": "A big cat exhibit", - "location": "North America" + "location": "North America", + "name": "Big Cat Exhibit" } ] }, @@ -1814,12 +1363,26 @@ "description": "The name of the exhibit" }, "description": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Description", "description": "The description of the exhibit" }, "location": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Location", "description": "The location of the exhibit" }, @@ -1829,15 +1392,22 @@ "title": "Created At", "description": "The date and time the record was created" }, - "modified_at": { + "updated_at": { "type": "string", "format": "date-time", - "title": "Modified At", + "title": "Updated At", "description": "The date and time the record was last modified" }, "deleted_at": { - "type": "string", - "format": "date-time", + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], "title": "Deleted At", "description": "The date and time the record was deleted" } @@ -1845,42 +1415,65 @@ "type": "object", "required": [ "id", - "name" + "name", + "created_at", + "updated_at" ], "title": "ExhibitsRead", - "description": "Exhibits model: read", + "description": "\n Exhibits model: read\n ", "examples": [ { - "name": "Big Cat Exhibit", + "created_at": "2021-01-01T00:00:00.000000", "description": "A big cat exhibit", - "location": "North America", "id": 1, - "created_at": "2021-01-01T00:00:00.000000", - "modified_at": "2021-01-02T09:12:34.567890" + "location": "North America", + "modified_at": "2021-01-02T09:12:34.567890", + "name": "Big Cat Exhibit" } ] }, "ExhibitsUpdate": { "properties": { "name": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Name", "description": "The name of the exhibit" }, "description": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Description", "description": "The description of the exhibit" }, "location": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Location", "description": "The location of the exhibit" } }, "type": "object", "title": "ExhibitsUpdate", - "description": "Exhibits model: update", + "description": "\n Exhibits model: update\n ", "examples": [ { "name": "Big Cat Exhibit" @@ -1926,11 +1519,11 @@ "timestamp" ], "title": "Health", - "description": "Health model", + "description": "\n Health model\n ", "examples": [ { - "status": "OK", "code": 200, + "status": "OK", "timestamp": "2021-05-01T12:00:00.000000+00:00" } ] @@ -1943,28 +1536,63 @@ "description": "The name of the staff" }, "job_title": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Job Title", "description": "The job title of the staff" }, "email": { - "type": "string", - "format": "email", + "anyOf": [ + { + "type": "string", + "format": "email" + }, + { + "type": "null" + } + ], "title": "Email", "description": "The email of the staff" }, "phone": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Phone", "description": "The phone number of the staff" }, "notes": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Notes", "description": "Optional notes regarding the staff member" }, "exhibit_id": { - "type": "integer", + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], "title": "Exhibit Id", "description": "The id of the exhibit" } @@ -1974,15 +1602,15 @@ "name" ], "title": "StaffCreate", - "description": "Staff model: create", + "description": "\n Staff model: create\n ", "examples": [ { - "name": "John Doe", - "job_title": "Zookeeper", "email": "big.cat.lover@gmail.com", - "phone": "555-555-5555", + "exhibit_id": 1, + "job_title": "Zookeeper", + "name": "John Doe", "notes": "John Doe is a great zookeeper and loves cats!", - "exhibit_id": 1 + "phone": "555-555-5555" } ] }, @@ -1999,28 +1627,63 @@ "description": "The name of the staff" }, "job_title": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Job Title", "description": "The job title of the staff" }, "email": { - "type": "string", - "format": "email", + "anyOf": [ + { + "type": "string", + "format": "email" + }, + { + "type": "null" + } + ], "title": "Email", "description": "The email of the staff" }, "phone": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Phone", "description": "The phone number of the staff" }, "notes": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Notes", "description": "Optional notes regarding the staff member" }, "exhibit_id": { - "type": "integer", + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], "title": "Exhibit Id", "description": "The id of the exhibit" }, @@ -2030,15 +1693,22 @@ "title": "Created At", "description": "The date and time the record was created" }, - "modified_at": { + "updated_at": { "type": "string", "format": "date-time", - "title": "Modified At", + "title": "Updated At", "description": "The date and time the record was last modified" }, "deleted_at": { - "type": "string", - "format": "date-time", + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], "title": "Deleted At", "description": "The date and time the record was deleted" } @@ -2046,66 +1716,113 @@ "type": "object", "required": [ "id", - "name" + "name", + "created_at", + "updated_at" ], "title": "StaffRead", - "description": "Staff model: read", + "description": "\n Staff model: read\n ", "examples": [ { - "name": "John Doe", - "job_title": "Zookeeper", + "created_at": "2021-01-01T00:00:00.000000", "email": "big.cat.lover@gmail.com", - "phone": "555-555-5555", - "notes": "John Doe is a great zookeeper and loves cats!", "exhibit_id": 1, "id": 1, - "created_at": "2021-01-01T00:00:00.000000", - "modified_at": "2021-01-02T09:12:34.567890" + "job_title": "Zookeeper", + "modified_at": "2021-01-02T09:12:34.567890", + "name": "John Doe", + "notes": "John Doe is a great zookeeper and loves cats!", + "phone": "555-555-5555" } ] }, "StaffUpdate": { "properties": { "name": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Name", "description": "The name of the staff" }, "job_title": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Job Title", "description": "The job title of the staff" }, "email": { - "type": "string", - "format": "email", + "anyOf": [ + { + "type": "string", + "format": "email" + }, + { + "type": "null" + } + ], "title": "Email", "description": "The email of the staff" }, "phone": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Phone", "description": "The phone number of the staff" }, "notes": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Notes", "description": "Optional notes regarding the staff member" }, "exhibit_id": { - "type": "integer", + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], "title": "Exhibit Id", "description": "The id of the exhibit" } }, "type": "object", + "required": [ + "name" + ], "title": "StaffUpdate", - "description": "Staff model: update", + "description": "\n Staff model: update\n ", "examples": [ { - "name": "John Doe", + "email": "big.cat.lover@gmail.com", "job_title": "Zookeeper", - "email": "big.cat.lover@gmail.com" + "name": "John Doe" } ] }, @@ -2121,17 +1838,38 @@ "title": "Password" }, "is_active": { - "type": "boolean", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], "title": "Is Active", "default": true }, "is_superuser": { - "type": "boolean", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], "title": "Is Superuser", "default": false }, "is_verified": { - "type": "boolean", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], "title": "Is Verified", "default": false } @@ -2142,7 +1880,7 @@ "password" ], "title": "UserCreate", - "description": "FastAPI Users - User Create Model" + "description": "\n FastAPI Users - User Create Model\n " }, "UserRead": { "properties": { @@ -2152,13 +1890,15 @@ "title": "Created At", "description": "The date and time the record was created" }, - "modified_at": { + "updated_at": { "type": "string", "format": "date-time", - "title": "Modified At", + "title": "Updated At", "description": "The date and time the record was last modified" }, "id": { + "type": "string", + "format": "uuid", "title": "Id" }, "email": { @@ -2184,38 +1924,13 @@ }, "type": "object", "required": [ + "created_at", + "updated_at", + "id", "email" ], "title": "UserRead", - "description": "FastAPI Users - User Read Model" - }, - "UserUpdate": { - "properties": { - "password": { - "type": "string", - "title": "Password" - }, - "email": { - "type": "string", - "format": "email", - "title": "Email" - }, - "is_active": { - "type": "boolean", - "title": "Is Active" - }, - "is_superuser": { - "type": "boolean", - "title": "Is Superuser" - }, - "is_verified": { - "type": "boolean", - "title": "Is Verified" - } - }, - "type": "object", - "title": "UserUpdate", - "description": "FastAPI Users - User Update Model" + "description": "\n FastAPI Users - User Read Model\n " }, "ValidationError": { "properties": { diff --git a/migrations/env.py b/migrations/env.py index 38c2a66..cb1eb79 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -8,18 +8,18 @@ from alembic import context from sqlalchemy.ext.asyncio import create_async_engine -from sqlmodel import SQLModel -from zoo.config import config as app_config +from zoo.config import app_config from zoo.models import __all_models__ +from zoo.models.base import Base config = context.config if config.config_file_name is not None: fileConfig(config.config_file_name) -# Add Zoo models to the metadata +# Register all Database Models with Alembic known_models = __all_models__ -target_metadata = SQLModel.metadata +target_metadata = Base.metadata if not app_config.DOCKER: app_config.rich_logging(loggers=[logging.getLogger()]) @@ -67,6 +67,7 @@ async def run_migrations_online() -> None: In this scenario we need to create an Engine and associate a connection with the context. """ + # raise ValueError(app_config.connection_string) engine = create_async_engine(app_config.connection_string, echo=app_config.DEBUG, future=True) async with engine.connect() as connection: await connection.run_sync(sync_run_migrations) diff --git a/migrations/script.py.mako b/migrations/script.py.mako index 0968b0c..8709d2d 100644 --- a/migrations/script.py.mako +++ b/migrations/script.py.mako @@ -10,7 +10,6 @@ from typing import Sequence, Union from alembic import op import sqlalchemy as sa -import sqlmodel ${imports if imports else ""} revision: str = ${repr(up_revision)} diff --git a/migrations/versions/2023_08_13_0009-9c5764f1931c_initial_migration.py b/migrations/versions/2023_08_13_0009-9c5764f1931c_initial_migration.py deleted file mode 100644 index ea8e07a..0000000 --- a/migrations/versions/2023_08_13_0009-9c5764f1931c_initial_migration.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Initial Migration - -Revision ID: 9c5764f1931c -Revises: -Create Date: 2023-08-13 00:09:09.862571 - -""" -from typing import Sequence, Union - -import sqlalchemy as sa -import sqlmodel -from alembic import op - -revision: str = "9c5764f1931c" -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - op.create_table( - "exhibits", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column("location", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=True, - ), - sa.Column( - "modified_at", - sa.DateTime(timezone=True), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=True, - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_exhibits_name"), "exhibits", ["name"], unique=True) - op.create_table( - "animals", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column("species", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column("exhibit_id", sa.Integer(), nullable=True), - sa.ForeignKeyConstraint( - ["exhibit_id"], - ["exhibits.id"], - ), - sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=True, - ), - sa.Column( - "modified_at", - sa.DateTime(timezone=True), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=True, - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_animals_name"), "animals", ["name"], unique=False) - op.create_table( - "staff", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column("job_title", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column("email", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column("phone", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column("notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column("exhibit_id", sa.Integer(), nullable=True), - sa.ForeignKeyConstraint( - ["exhibit_id"], - ["exhibits.id"], - ), - sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=True, - ), - sa.Column( - "modified_at", - sa.DateTime(timezone=True), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=True, - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_staff_name"), "staff", ["name"], unique=True) - - -def downgrade() -> None: - op.drop_index(op.f("ix_staff_name"), table_name="staff") - op.drop_table("staff") - op.drop_index(op.f("ix_animals_name"), table_name="animals") - op.drop_table("animals") - op.drop_index(op.f("ix_exhibits_name"), table_name="exhibits") - op.drop_table("exhibits") diff --git a/migrations/versions/2023_08_13_0025-5ff3a5bf3a14_seed_data.py b/migrations/versions/2023_08_13_0025-5ff3a5bf3a14_seed_data.py deleted file mode 100644 index 83ab9d7..0000000 --- a/migrations/versions/2023_08_13_0025-5ff3a5bf3a14_seed_data.py +++ /dev/null @@ -1,123 +0,0 @@ -"""Seed Data - -Revision ID: 5ff3a5bf3a14 -Revises: 9c5764f1931c -Create Date: 2023-08-13 00:25:20.348243 - -""" - -from typing import List, Sequence, Union - -from alembic import op -from pydantic import EmailStr - -from zoo.models import Animals -from zoo.models.animals import AnimalsCreate -from zoo.models.exhibits import Exhibits, ExhibitsCreate -from zoo.models.staff import Staff, StaffCreate - -revision: str = "5ff3a5bf3a14" -down_revision: Union[str, None] = "9c5764f1931c" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - -exhibits = [ - ExhibitsCreate( - name="Big Cat Exhibit", description="A big cat exhibit", location="North America" - ), - ExhibitsCreate(name="Bird Exhibit", description="A bird exhibit", location="North America"), - ExhibitsCreate( - name="Reptile Exhibit", description="A reptile exhibit", location="North America" - ), - ExhibitsCreate( - name="Aquatic Exhibit", description="An aquatic exhibit", location="North America" - ), -] - -staff_members: List[StaffCreate] = [ - StaffCreate( - name="John Doe", - job_title="Zookeeper", - email=EmailStr("john-does-loves-kitties@gmail.com"), - phone="555-555-5555", - notes="John Doe is a great zookeeper and loves cats!", - exhibit_id=1, - ), - StaffCreate( - name="Jane Doe", - job_title="Zookeeper", - email=EmailStr("jane-doe@yahoo.com"), - phone="555-444-6666", - notes="Jane Doe is a highly skilled bird keeper!", - exhibit_id=3, - ), -] - -animals = [ - AnimalsCreate( - name="Lion", - description="Ferocious kitty with mane", - species="Panthera leo", - exhibit_id=1, - ), - AnimalsCreate( - name="Tiger", - description="Ferocious kitty with stripes", - species="Panthera tigris", - exhibit_id=1, - ), - AnimalsCreate( - name="Cheetah", - description="Ferocious fast kitty", - species="Acinonyx jubatus", - exhibit_id=1, - ), - AnimalsCreate( - name="Leopard", - description="Ferocious spotted kitty", - species="Panthera pardus", - exhibit_id=1, - ), - AnimalsCreate( - name="Cougar", - description="Ferocious mountain kitty", - species="Puma concolor", - exhibit_id=1, - ), -] - - -def upgrade() -> None: - """ - Seed the database with initial data. - """ - op.bulk_insert( - table=Exhibits.__table__, - rows=[exhibit.dict(exclude_unset=True) for exhibit in exhibits], - ) - op.bulk_insert( - table=Staff.__table__, - rows=[staff_member.dict(exclude_unset=True) for staff_member in staff_members], - ) - op.bulk_insert( - table=Animals.__table__, - rows=[animal.dict(exclude_unset=True) for animal in animals], - ) - - -def downgrade() -> None: - """ - Remove the initial data from the database. - """ - animal_delete = Animals.__table__.delete( - Animals.name.in_([animal.name for animal in animals]) # type: ignore[attr-defined] - ) - staff_delete = Staff.__table__.delete( - Staff.name.in_([staff_member.name for staff_member in staff_members]) # type: ignore[attr-defined] - ) - exhibit_delete = Exhibits.__table__.delete( - Exhibits.name.in_([exhibit.name for exhibit in exhibits]) # type: ignore[attr-defined] - ) - op.execute(animal_delete) # type: ignore[arg-type] - op.execute(staff_delete) # type: ignore[arg-type] - op.execute(exhibit_delete) # type: ignore[arg-type] diff --git a/migrations/versions/2023_08_17_1904-47783d8459db_fastapi_users.py b/migrations/versions/2023_08_17_1904-47783d8459db_fastapi_users.py deleted file mode 100644 index d14635c..0000000 --- a/migrations/versions/2023_08_17_1904-47783d8459db_fastapi_users.py +++ /dev/null @@ -1,76 +0,0 @@ -"""fastapi-users - -Revision ID: 47783d8459db -Revises: 5ff3a5bf3a14 -Create Date: 2023-08-17 19:04:26.774702 - -""" - -from typing import Sequence, Union - -import fastapi_users_db_sqlmodel -import sqlalchemy as sa -import sqlmodel -from alembic import op - -revision: str = "47783d8459db" -down_revision: Union[str, None] = "5ff3a5bf3a14" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """ - Upgrade the database - """ - op.create_table( - "user", - sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False), - sa.Column("email", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column("hashed_password", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column("is_active", sa.Boolean(), nullable=False), - sa.Column("is_superuser", sa.Boolean(), nullable=False), - sa.Column("is_verified", sa.Boolean(), nullable=False), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=True, - ), - sa.Column( - "modified_at", - sa.DateTime(timezone=True), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=True, - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True) - op.create_table( - "access_token", - sa.Column("token", sa.String(length=43), nullable=False), - sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=False), - sa.Column( - "created_at", - fastapi_users_db_sqlmodel.generics.TIMESTAMPAware(timezone=True), - nullable=False, - ), - sa.ForeignKeyConstraint( - ["user_id"], - ["user.id"], - ), - sa.PrimaryKeyConstraint("token"), - ) - op.create_index( - op.f("ix_access_token_created_at"), "access_token", ["created_at"], unique=False - ) - - -def downgrade() -> None: - """ - Rollback the database upgrade - """ - op.drop_index(op.f("ix_access_token_created_at"), table_name="access_token") - op.drop_table("access_token") - op.drop_index(op.f("ix_user_email"), table_name="user") - op.drop_table("user") diff --git a/migrations/versions/2023_08_20_1729-e5fccc3522ce_initial_migration.py b/migrations/versions/2023_08_20_1729-e5fccc3522ce_initial_migration.py new file mode 100644 index 0000000..511bd90 --- /dev/null +++ b/migrations/versions/2023_08_20_1729-e5fccc3522ce_initial_migration.py @@ -0,0 +1,157 @@ +"""Initial Migration + +Revision ID: e5fccc3522ce +Revises: +Create Date: 2023-08-20 17:29:33.690933 + +""" + +from typing import Sequence, Union + +import fastapi_users_db_sqlalchemy +import sqlalchemy as sa +from alembic import op + +revision: str = "e5fccc3522ce" +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """ + Upgrade the database + """ + op.create_table( + "exhibits", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("location", sa.String(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "user", + sa.Column("id", fastapi_users_db_sqlalchemy.generics.GUID(), nullable=False), + sa.Column("email", sa.String(length=320), nullable=False), + sa.Column("hashed_password", sa.String(length=1024), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.Column("is_superuser", sa.Boolean(), nullable=False), + sa.Column("is_verified", sa.Boolean(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True) + op.create_table( + "access_token", + sa.Column("user_id", fastapi_users_db_sqlalchemy.generics.GUID(), nullable=False), + sa.Column("token", sa.String(length=43), nullable=False), + sa.Column( + "created_at", + fastapi_users_db_sqlalchemy.generics.TIMESTAMPAware(timezone=True), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.ForeignKeyConstraint(["user_id"], ["user.id"], ondelete="cascade"), + sa.PrimaryKeyConstraint("token"), + ) + op.create_index( + op.f("ix_access_token_created_at"), "access_token", ["created_at"], unique=False + ) + op.create_table( + "animals", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("species", sa.String(), nullable=True), + sa.Column("exhibit_id", sa.Integer(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint( + ["exhibit_id"], + ["exhibits.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "staff", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("job_title", sa.String(), nullable=True), + sa.Column("email", sa.String(), nullable=True), + sa.Column("phone", sa.String(), nullable=True), + sa.Column("notes", sa.String(), nullable=True), + sa.Column("exhibit_id", sa.Integer(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint( + ["exhibit_id"], + ["exhibits.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + + +def downgrade() -> None: + """ + Rollback the database upgrade + """ + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("staff") + op.drop_table("animals") + op.drop_index(op.f("ix_access_token_created_at"), table_name="access_token") + op.drop_table("access_token") + op.drop_index(op.f("ix_user_email"), table_name="user") + op.drop_table("user") + op.drop_table("exhibits") diff --git a/migrations/versions/2023_08_20_1730-e69c9264375d_seed_data.py b/migrations/versions/2023_08_20_1730-e69c9264375d_seed_data.py new file mode 100644 index 0000000..e92647b --- /dev/null +++ b/migrations/versions/2023_08_20_1730-e69c9264375d_seed_data.py @@ -0,0 +1,114 @@ +"""Seed Data + +Revision ID: e69c9264375d +Revises: e5fccc3522ce +Create Date: 2023-08-20 17:30:29.527566 + +""" + +from typing import Sequence, Union + +from alembic import op +from sqlalchemy import delete +from sqlalchemy.orm import Session + +from zoo.models.animals import Animals +from zoo.models.exhibits import Exhibits +from zoo.models.staff import Staff + +revision: str = "e69c9264375d" +down_revision: Union[str, None] = "e5fccc3522ce" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +exhibits = [ + Exhibits(name="Big Cat Exhibit", description="A big cat exhibit", location="North America"), + Exhibits(name="Bird Exhibit", description="A bird exhibit", location="North America"), + Exhibits(name="Reptile Exhibit", description="A reptile exhibit", location="North America"), + Exhibits(name="Aquatic Exhibit", description="An aquatic exhibit", location="North America"), +] + +staff_members = [ + Staff( + name="John Doe", + job_title="Zookeeper", + email="john-does-loves-kitties@gmail.com", + phone="555-555-5555", + notes="John Doe is a great zookeeper and loves cats!", + exhibit_id=1, + ), + Staff( + name="Jane Doe", + job_title="Zookeeper", + email="jane-doe@yahoo.com", + phone="555-444-6666", + notes="Jane Doe is a highly skilled bird keeper!", + exhibit_id=3, + ), +] + +animals = [ + Animals( + name="Lion", + description="Ferocious kitty with mane", + species="Panthera leo", + exhibit_id=1, + ), + Animals( + name="Tiger", + description="Ferocious kitty with stripes", + species="Panthera tigris", + exhibit_id=1, + ), + Animals( + name="Cheetah", + description="Ferocious fast kitty", + species="Acinonyx jubatus", + exhibit_id=1, + ), + Animals( + name="Leopard", + description="Ferocious spotted kitty", + species="Panthera pardus", + exhibit_id=1, + ), + Animals( + name="Cougar", + description="Ferocious mountain kitty", + species="Puma concolor", + exhibit_id=1, + ), +] + + +def upgrade() -> None: + """ + Seed the database with initial data. + """ + connection = op.get_bind() + session = Session(bind=connection) + with session.begin(): + for exhibit in exhibits: + session.add(exhibit) + for staff_member in staff_members: + session.add(staff_member) + for animal in animals: + session.add(animal) + session.commit() + + +def downgrade() -> None: + """ + Rollback the database upgrade + """ + animal_delete = delete(Animals).where(Animals.name.in_([animal.name for animal in animals])) + staff_delete = delete(Staff).where( + Staff.name.in_([staff_member.name for staff_member in staff_members]) + ) + exhibit_delete = delete(Exhibits).where( + Exhibits.name.in_([exhibit.name for exhibit in exhibits]) + ) + op.execute(animal_delete) # type: ignore[arg-type] + op.execute(staff_delete) # type: ignore[arg-type] + op.execute(exhibit_delete) # type: ignore[arg-type] diff --git a/pyproject.toml b/pyproject.toml index 9bc1b91..122f505 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,13 +18,11 @@ classifiers = [ ] dependencies = [ "fastapi~=0.100.1", - # "fastapi~=0.98.0", # OpenAPI 3.0.2 for now - "fastapi-users~=12.1.1", - "fastapi-users-db-sqlmodel~=0.3.0", - "sqlmodel==0.0.8", - "pydantic[email]~=1.10.12", - # "pydantic==2.1.1", # TODO: Upgrade to > 1 when SQLModel supports it - # "pydantic-extra-types~=2.0.0", + "fastapi-users[sqlalchemy]~=12.1.1", + "pydantic[email]==2.1.1", + "pydantic-settings~=2.0.3", + "pydantic-extra-types~=2.0.0", + "sqlalchemy[asyncio]~=2.0.20", "aiosqlite~=0.19.0", "asyncpg~=0.28.0", "greenlet~=2.0.2", @@ -32,7 +30,8 @@ dependencies = [ "gunicorn~=21.2.0", "alembic~=1.11.2", "httpx~=0.24.1", - "rich~=13.5.2" + "rich~=13.5.2", + "click~=8.1.7" ] description = "An asynchronous zoo API, powered by FastAPI and SQLModel" dynamic = ["version"] @@ -141,15 +140,15 @@ detached = false all = ["docs"] clients = ["py", "ts"] docs = ["openapi"] -openapi = "python -m zoo.app --openapi" +openapi = "python -m zoo openapi" py = [ "rm -rf ./dist/clients/python", """ - docker run --rm -v ./:/local openapitools/openapi-generator-cli generate \ - -i /local/docs/openapi.json \ - -g python \ - -o /local/dist/clients/python - """ + docker run --rm -v ./:/local openapitools/openapi-generator-cli generate \ + -i /local/docs/openapi.json \ + -g python \ + -o /local/dist/clients/python + """ ] release = [ "npm install --prefix .github/semantic_release/", diff --git a/requirements.txt b/requirements.txt index bb5e06b..02e262a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,10 @@ alembic==1.11.3 \ --hash=sha256:3db4ce81a9072e1b5aa44c2d202add24553182672a12daf21608d6f62a8f9cf9 \ --hash=sha256:d6c96c2482740592777c400550a523bc7a9aada4e210cae2e733354ddae6f6f8 # via zoo (pyproject.toml) +annotated-types==0.5.0 \ + --hash=sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802 \ + --hash=sha256:58da39888f92c276ad970249761ebea80ba544b77acddaa1a4d6cf78287d45fd + # via pydantic anyio==3.7.1 \ --hash=sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780 \ --hash=sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5 @@ -155,10 +159,12 @@ cffi==1.15.1 \ --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \ --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0 # via cryptography -click==8.1.6 \ - --hash=sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd \ - --hash=sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5 - # via uvicorn +click==8.1.7 \ + --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ + --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de + # via + # uvicorn + # zoo (pyproject.toml) cryptography==41.0.3 \ --hash=sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306 \ --hash=sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84 \ @@ -200,16 +206,16 @@ fastapi==0.100.1 \ # via # fastapi-users # zoo (pyproject.toml) -fastapi-users==12.1.1 \ +fastapi-users[sqlalchemy]==12.1.1 \ --hash=sha256:1118f25284b42955414142bf3ecfc47feb6c5864a4b52cffde4a91f705889c29 \ --hash=sha256:d3ba4278799a53137c4414725840847874cead4578c42aad721de3e2aae42805 # via - # fastapi-users-db-sqlmodel + # fastapi-users-db-sqlalchemy # zoo (pyproject.toml) -fastapi-users-db-sqlmodel==0.3.0 \ - --hash=sha256:696508a65b1577897fbf839ba2b5ab3ead2e2df2d6beccda104b52580b9475c9 \ - --hash=sha256:917a51a813b45d25347a23244e2d873b489e67b7d28a770ef4fa27369b9544f3 - # via zoo (pyproject.toml) +fastapi-users-db-sqlalchemy==6.0.1 \ + --hash=sha256:d1050ec31eb75e8c4fa9abafa4addaf0baf5c97afeea2f0f910ea55e2451fcad \ + --hash=sha256:f0ef9fe3250453712d25c13170700c80fa205867ce7add7ef391c384ec27cbe1 + # via fastapi-users greenlet==2.0.2 \ --hash=sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a \ --hash=sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a \ @@ -272,7 +278,7 @@ greenlet==2.0.2 \ --hash=sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1 \ --hash=sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526 # via - # fastapi-users-db-sqlmodel + # sqlalchemy # zoo (pyproject.toml) gunicorn==21.2.0 \ --hash=sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0 \ @@ -379,47 +385,125 @@ pycparser==2.21 \ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 # via cffi -pydantic[email]==1.10.12 \ - --hash=sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303 \ - --hash=sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe \ - --hash=sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47 \ - --hash=sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494 \ - --hash=sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33 \ - --hash=sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86 \ - --hash=sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d \ - --hash=sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c \ - --hash=sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a \ - --hash=sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565 \ - --hash=sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb \ - --hash=sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62 \ - --hash=sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62 \ - --hash=sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0 \ - --hash=sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523 \ - --hash=sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d \ - --hash=sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405 \ - --hash=sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f \ - --hash=sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b \ - --hash=sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718 \ - --hash=sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed \ - --hash=sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb \ - --hash=sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5 \ - --hash=sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc \ - --hash=sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942 \ - --hash=sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe \ - --hash=sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246 \ - --hash=sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350 \ - --hash=sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303 \ - --hash=sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09 \ - --hash=sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33 \ - --hash=sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8 \ - --hash=sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a \ - --hash=sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1 \ - --hash=sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6 \ - --hash=sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d +pydantic[email]==2.1.1 \ + --hash=sha256:22d63db5ce4831afd16e7c58b3192d3faf8f79154980d9397d9867254310ba4b \ + --hash=sha256:43bdbf359d6304c57afda15c2b95797295b702948082d4c23851ce752f21da70 # via # fastapi - # sqlmodel + # pydantic-extra-types + # pydantic-settings # zoo (pyproject.toml) +pydantic-core==2.4.0 \ + --hash=sha256:01947ad728f426fa07fcb26457ebf90ce29320259938414bc0edd1476e75addb \ + --hash=sha256:0455876d575a35defc4da7e0a199596d6c773e20d3d42fa1fc29f6aa640369ed \ + --hash=sha256:047580388644c473b934d27849f8ed8dbe45df0adb72104e78b543e13bf69762 \ + --hash=sha256:04922fea7b13cd480586fa106345fe06e43220b8327358873c22d8dfa7a711c7 \ + --hash=sha256:08f89697625e453421401c7f661b9d1eb4c9e4c0a12fd256eeb55b06994ac6af \ + --hash=sha256:0a507d7fa44688bbac76af6521e488b3da93de155b9cba6f2c9b7833ce243d59 \ + --hash=sha256:0d726108c1c0380b88b6dd4db559f0280e0ceda9e077f46ff90bc85cd4d03e77 \ + --hash=sha256:12ef6838245569fd60a179fade81ca4b90ae2fa0ef355d616f519f7bb27582db \ + --hash=sha256:153a61ac4030fa019b70b31fb7986461119230d3ba0ab661c757cfea652f4332 \ + --hash=sha256:16468bd074fa4567592d3255bf25528ed41e6b616d69bf07096bdb5b66f947d1 \ + --hash=sha256:17156abac20a9feed10feec867fddd91a80819a485b0107fe61f09f2117fe5f3 \ + --hash=sha256:1927f0e15d190f11f0b8344373731e28fd774c6d676d8a6cfadc95c77214a48b \ + --hash=sha256:1e8a7c62d15a5c4b307271e4252d76ebb981d6251c6ecea4daf203ef0179ea4f \ + --hash=sha256:2ad538b7e07343001934417cdc8584623b4d8823c5b8b258e75ec8d327cec969 \ + --hash=sha256:2ca4687dd996bde7f3c420def450797feeb20dcee2b9687023e3323c73fc14a2 \ + --hash=sha256:2edef05b63d82568b877002dc4cb5cc18f8929b59077120192df1e03e0c633f8 \ + --hash=sha256:2f9ea0355f90db2a76af530245fa42f04d98f752a1236ed7c6809ec484560d5b \ + --hash=sha256:30527d173e826f2f7651f91c821e337073df1555e3b5a0b7b1e2c39e26e50678 \ + --hash=sha256:32a1e0352558cd7ccc014ffe818c7d87b15ec6145875e2cc5fa4bb7351a1033d \ + --hash=sha256:3534118289e33130ed3f1cc487002e8d09b9f359be48b02e9cd3de58ce58fba9 \ + --hash=sha256:36ba9e728588588f0196deaf6751b9222492331b5552f865a8ff120869d372e0 \ + --hash=sha256:382f0baa044d674ad59455a5eff83d7965572b745cc72df35c52c2ce8c731d37 \ + --hash=sha256:394f12a2671ff8c4dfa2e85be6c08be0651ad85bc1e6aa9c77c21671baaf28cd \ + --hash=sha256:3ba2c9c94a9176f6321a879c8b864d7c5b12d34f549a4c216c72ce213d7d953c \ + --hash=sha256:3ded19dcaefe2f6706d81e0db787b59095f4ad0fbadce1edffdf092294c8a23f \ + --hash=sha256:3fcf529382b282a30b466bd7af05be28e22aa620e016135ac414f14e1ee6b9e1 \ + --hash=sha256:43a405ce520b45941df9ff55d0cd09762017756a7b413bbad3a6e8178e64a2c2 \ + --hash=sha256:453862ab268f6326b01f067ed89cb3a527d34dc46f6f4eeec46a15bbc706d0da \ + --hash=sha256:4665f7ed345012a8d2eddf4203ef145f5f56a291d010382d235b94e91813f88a \ + --hash=sha256:478f5f6d7e32bd4a04d102160efb2d389432ecf095fe87c555c0a6fc4adfc1a4 \ + --hash=sha256:49db206eb8fdc4b4f30e6e3e410584146d813c151928f94ec0db06c4f2595538 \ + --hash=sha256:4b262bbc13022f2097c48a21adcc360a81d83dc1d854c11b94953cd46d7d3c07 \ + --hash=sha256:4cbe929efa77a806e8f1a97793f2dc3ea3475ae21a9ed0f37c21320fe93f6f50 \ + --hash=sha256:4e562cc63b04636cde361fd47569162f1daa94c759220ff202a8129902229114 \ + --hash=sha256:546064c55264156b973b5e65e5fafbe5e62390902ce3cf6b4005765505e8ff56 \ + --hash=sha256:54df7df399b777c1fd144f541c95d351b3aa110535a6810a6a569905d106b6f3 \ + --hash=sha256:56a85fa0dab1567bd0cac10f0c3837b03e8a0d939e6a8061a3a420acd97e9421 \ + --hash=sha256:57a53a75010c635b3ad6499e7721eaa3b450e03f6862afe2dbef9c8f66e46ec8 \ + --hash=sha256:584a7a818c84767af16ce8bda5d4f7fedb37d3d231fc89928a192f567e4ef685 \ + --hash=sha256:5fd905a69ac74eaba5041e21a1e8b1a479dab2b41c93bdcc4c1cede3c12a8d86 \ + --hash=sha256:61d4e713f467abcdd59b47665d488bb898ad3dd47ce7446522a50e0cbd8e8279 \ + --hash=sha256:6213b471b68146af97b8551294e59e7392c2117e28ffad9c557c65087f4baee3 \ + --hash=sha256:63797499a219d8e81eb4e0c42222d0a4c8ec896f5c76751d4258af95de41fdf1 \ + --hash=sha256:64e8012ad60a5f0da09ed48725e6e923d1be25f2f091a640af6079f874663813 \ + --hash=sha256:664402ef0c238a7f8a46efb101789d5f2275600fb18114446efec83cfadb5b66 \ + --hash=sha256:68199ada7c310ddb8c76efbb606a0de656b40899388a7498954f423e03fc38be \ + --hash=sha256:69159afc2f2dc43285725f16143bc5df3c853bc1cb7df6021fce7ef1c69e8171 \ + --hash=sha256:6f855bcc96ed3dd56da7373cfcc9dcbabbc2073cac7f65c185772d08884790ce \ + --hash=sha256:6feb4b64d11d5420e517910d60a907d08d846cacaf4e029668725cd21d16743c \ + --hash=sha256:72f1216ca8cef7b8adacd4c4c6b89c3b0c4f97503197f5284c80f36d6e4edd30 \ + --hash=sha256:77dadc764cf7c5405e04866181c5bd94a447372a9763e473abb63d1dfe9b7387 \ + --hash=sha256:782fced7d61469fd1231b184a80e4f2fa7ad54cd7173834651a453f96f29d673 \ + --hash=sha256:79262be5a292d1df060f29b9a7cdd66934801f987a817632d7552534a172709a \ + --hash=sha256:7aa82d483d5fb867d4fb10a138ffd57b0f1644e99f2f4f336e48790ada9ada5e \ + --hash=sha256:853f103e2b9a58832fdd08a587a51de8b552ae90e1a5d167f316b7eabf8d7dde \ + --hash=sha256:867d3eea954bea807cabba83cfc939c889a18576d66d197c60025b15269d7cc0 \ + --hash=sha256:878a5017d93e776c379af4e7b20f173c82594d94fa073059bcc546789ad50bf8 \ + --hash=sha256:884235507549a6b2d3c4113fb1877ae263109e787d9e0eb25c35982ab28d0399 \ + --hash=sha256:8c938c96294d983dcf419b54dba2d21056959c22911d41788efbf949a29ae30d \ + --hash=sha256:8efc1be43b036c2b6bcfb1451df24ee0ddcf69c31351003daf2699ed93f5687b \ + --hash=sha256:8fba0aff4c407d0274e43697e785bcac155ad962be57518d1c711f45e72da70f \ + --hash=sha256:90f3785146f701e053bb6b9e8f53acce2c919aca91df88bd4975be0cb926eb41 \ + --hash=sha256:9137289de8fe845c246a8c3482dd0cb40338846ba683756d8f489a4bd8fddcae \ + --hash=sha256:9206c14a67c38de7b916e486ae280017cf394fa4b1aa95cfe88621a4e1d79725 \ + --hash=sha256:94d2b36a74623caab262bf95f0e365c2c058396082bd9d6a9e825657d0c1e7fa \ + --hash=sha256:97c6349c81cee2e69ef59eba6e6c08c5936e6b01c2d50b9e4ac152217845ae09 \ + --hash=sha256:a027f41c5008571314861744d83aff75a34cf3a07022e0be32b214a5bc93f7f1 \ + --hash=sha256:a08fd490ba36d1fbb2cd5dcdcfb9f3892deb93bd53456724389135712b5fc735 \ + --hash=sha256:a297c0d6c61963c5c3726840677b798ca5b7dfc71bc9c02b9a4af11d23236008 \ + --hash=sha256:a4ea23b07f29487a7bef2a869f68c7ee0e05424d81375ce3d3de829314c6b5ec \ + --hash=sha256:a8b7acd04896e8f161e1500dc5f218017db05c1d322f054e89cbd089ce5d0071 \ + --hash=sha256:ac2b680de398f293b68183317432b3d67ab3faeba216aec18de0c395cb5e3060 \ + --hash=sha256:af24ad4fbaa5e4a2000beae0c3b7fd1c78d7819ab90f9370a1cfd8998e3f8a3c \ + --hash=sha256:af788b64e13d52fc3600a68b16d31fa8d8573e3ff2fc9a38f8a60b8d94d1f012 \ + --hash=sha256:b013c7861a7c7bfcec48fd709513fea6f9f31727e7a0a93ca0dd12e056740717 \ + --hash=sha256:b2799c2eaf182769889761d4fb4d78b82bc47dae833799fedbf69fc7de306faa \ + --hash=sha256:b27f3e67f6e031f6620655741b7d0d6bebea8b25d415924b3e8bfef2dd7bd841 \ + --hash=sha256:b7206e41e04b443016e930e01685bab7a308113c0b251b3f906942c8d4b48fcb \ + --hash=sha256:b85778308bf945e9b33ac604e6793df9b07933108d20bdf53811bc7c2798a4af \ + --hash=sha256:bd7d1dde70ff3e09e4bc7a1cbb91a7a538add291bfd5b3e70ef1e7b45192440f \ + --hash=sha256:be86c2eb12fb0f846262ace9d8f032dc6978b8cb26a058920ecb723dbcb87d05 \ + --hash=sha256:bf10963d8aed8bbe0165b41797c9463d4c5c8788ae6a77c68427569be6bead41 \ + --hash=sha256:c1375025f0bfc9155286ebae8eecc65e33e494c90025cda69e247c3ccd2bab00 \ + --hash=sha256:c5d8e764b5646623e57575f624f8ebb8f7a9f7fd1fae682ef87869ca5fec8dcf \ + --hash=sha256:cba5ad5eef02c86a1f3da00544cbc59a510d596b27566479a7cd4d91c6187a11 \ + --hash=sha256:cc086ddb6dc654a15deeed1d1f2bcb1cb924ebd70df9dca738af19f64229b06c \ + --hash=sha256:d0c2b713464a8e263a243ae7980d81ce2de5ac59a9f798a282e44350b42dc516 \ + --hash=sha256:d93aedbc4614cc21b9ab0d0c4ccd7143354c1f7cffbbe96ae5216ad21d1b21b5 \ + --hash=sha256:d9610b47b5fe4aacbbba6a9cb5f12cbe864eec99dbfed5710bd32ef5dd8a5d5b \ + --hash=sha256:da055a1b0bfa8041bb2ff586b2cb0353ed03944a3472186a02cc44a557a0e661 \ + --hash=sha256:dd2429f7635ad4857b5881503f9c310be7761dc681c467a9d27787b674d1250a \ + --hash=sha256:de39eb3bab93a99ddda1ac1b9aa331b944d8bcc4aa9141148f7fd8ee0299dafc \ + --hash=sha256:e40b1e97edd3dc127aa53d8a5e539a3d0c227d71574d3f9ac1af02d58218a122 \ + --hash=sha256:e412607ca89a0ced10758dfb8f9adcc365ce4c1c377e637c01989a75e9a9ec8a \ + --hash=sha256:e953353180bec330c3b830891d260b6f8e576e2d18db3c78d314e56bb2276066 \ + --hash=sha256:ec3473c9789cc00c7260d840c3db2c16dbfc816ca70ec87a00cddfa3e1a1cdd5 \ + --hash=sha256:efff8b6761a1f6e45cebd1b7a6406eb2723d2d5710ff0d1b624fe11313693989 \ + --hash=sha256:f773b39780323a0499b53ebd91a28ad11cde6705605d98d999dfa08624caf064 \ + --hash=sha256:fa8e48001b39d54d97d7b380a0669fa99fc0feeb972e35a2d677ba59164a9a22 \ + --hash=sha256:ff246c0111076c8022f9ba325c294f2cb5983403506989253e04dbae565e019b \ + --hash=sha256:ffe18407a4d000c568182ce5388bbbedeb099896904e43fc14eee76cfae6dec5 + # via pydantic +pydantic-extra-types==2.0.0 \ + --hash=sha256:137ddacb168d95ea77591dbb3739ec4da5eeac0fc4df7f797371d9904451a178 \ + --hash=sha256:63e5109f00815e71fff2b82090ff0523baef6b8a51889356fd984ef50c184e64 + # via zoo (pyproject.toml) +pydantic-settings==2.0.3 \ + --hash=sha256:962dc3672495aad6ae96a4390fac7e593591e144625e5112d359f8f67fb75945 \ + --hash=sha256:ddd907b066622bd67603b75e2ff791875540dc485b7307c4fffc015719da8625 + # via zoo (pyproject.toml) pygments==2.16.1 \ --hash=sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692 \ --hash=sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29 @@ -428,6 +512,10 @@ pyjwt[crypto]==2.8.0 \ --hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \ --hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320 # via fastapi-users +python-dotenv==1.0.0 \ + --hash=sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba \ + --hash=sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a + # via pydantic-settings python-multipart==0.0.6 \ --hash=sha256:e9925a80bb668529f1b67c7fdb0a5dacdd7cbfc6fb0bff3ea443fe22bdd62132 \ --hash=sha256:ee698bab5ef148b0a760751c261902cd096e57e10558e11aca17646b74ee1c18 @@ -443,60 +531,51 @@ sniffio==1.3.0 \ # anyio # httpcore # httpx -sqlalchemy==1.4.41 \ - --hash=sha256:0002e829142b2af00b4eaa26c51728f3ea68235f232a2e72a9508a3116bd6ed0 \ - --hash=sha256:0005bd73026cd239fc1e8ccdf54db58b6193be9a02b3f0c5983808f84862c767 \ - --hash=sha256:0292f70d1797e3c54e862e6f30ae474014648bc9c723e14a2fda730adb0a9791 \ - --hash=sha256:036d8472356e1d5f096c5e0e1a7e0f9182140ada3602f8fff6b7329e9e7cfbcd \ - --hash=sha256:05f0de3a1dc3810a776275763764bb0015a02ae0f698a794646ebc5fb06fad33 \ - --hash=sha256:0990932f7cca97fece8017414f57fdd80db506a045869d7ddf2dda1d7cf69ecc \ - --hash=sha256:13e397a9371ecd25573a7b90bd037db604331cf403f5318038c46ee44908c44d \ - --hash=sha256:14576238a5f89bcf504c5f0a388d0ca78df61fb42cb2af0efe239dc965d4f5c9 \ - --hash=sha256:199a73c31ac8ea59937cc0bf3dfc04392e81afe2ec8a74f26f489d268867846c \ - --hash=sha256:2082a2d2fca363a3ce21cfa3d068c5a1ce4bf720cf6497fb3a9fc643a8ee4ddd \ - --hash=sha256:22ff16cedab5b16a0db79f1bc99e46a6ddececb60c396562e50aab58ddb2871c \ - --hash=sha256:2307495d9e0ea00d0c726be97a5b96615035854972cc538f6e7eaed23a35886c \ - --hash=sha256:2ad2b727fc41c7f8757098903f85fafb4bf587ca6605f82d9bf5604bd9c7cded \ - --hash=sha256:2d6495f84c4fd11584f34e62f9feec81bf373787b3942270487074e35cbe5330 \ - --hash=sha256:361f6b5e3f659e3c56ea3518cf85fbdae1b9e788ade0219a67eeaaea8a4e4d2a \ - --hash=sha256:3e2ef592ac3693c65210f8b53d0edcf9f4405925adcfc031ff495e8d18169682 \ - --hash=sha256:4676d51c9f6f6226ae8f26dc83ec291c088fe7633269757d333978df78d931ab \ - --hash=sha256:4ba7e122510bbc07258dc42be6ed45997efdf38129bde3e3f12649be70683546 \ - --hash=sha256:5102fb9ee2c258a2218281adcb3e1918b793c51d6c2b4666ce38c35101bb940e \ - --hash=sha256:5323252be2bd261e0aa3f33cb3a64c45d76829989fa3ce90652838397d84197d \ - --hash=sha256:58bb65b3274b0c8a02cea9f91d6f44d0da79abc993b33bdedbfec98c8440175a \ - --hash=sha256:59bdc291165b6119fc6cdbc287c36f7f2859e6051dd923bdf47b4c55fd2f8bd0 \ - --hash=sha256:5facb7fd6fa8a7353bbe88b95695e555338fb038ad19ceb29c82d94f62775a05 \ - --hash=sha256:639e1ae8d48b3c86ffe59c0daa9a02e2bfe17ca3d2b41611b30a0073937d4497 \ - --hash=sha256:8eb8897367a21b578b26f5713833836f886817ee2ffba1177d446fa3f77e67c8 \ - --hash=sha256:90484a2b00baedad361402c257895b13faa3f01780f18f4a104a2f5c413e4536 \ - --hash=sha256:9c56e19780cd1344fcd362fd6265a15f48aa8d365996a37fab1495cae8fcd97d \ - --hash=sha256:b67fc780cfe2b306180e56daaa411dd3186bf979d50a6a7c2a5b5036575cbdbb \ - --hash=sha256:c0dcf127bb99458a9d211e6e1f0f3edb96c874dd12f2503d4d8e4f1fd103790b \ - --hash=sha256:c23d64a0b28fc78c96289ffbd0d9d1abd48d267269b27f2d34e430ea73ce4b26 \ - --hash=sha256:ccfd238f766a5bb5ee5545a62dd03f316ac67966a6a658efb63eeff8158a4bbf \ - --hash=sha256:cd767cf5d7252b1c88fcfb58426a32d7bd14a7e4942497e15b68ff5d822b41ad \ - --hash=sha256:ce8feaa52c1640de9541eeaaa8b5fb632d9d66249c947bb0d89dd01f87c7c288 \ - --hash=sha256:d2e054aed4645f9b755db85bc69fc4ed2c9020c19c8027976f66576b906a74f1 \ - --hash=sha256:e16c2be5cb19e2c08da7bd3a87fed2a0d4e90065ee553a940c4fc1a0fb1ab72b \ - --hash=sha256:e4b12e3d88a8fffd0b4ca559f6d4957ed91bd4c0613a4e13846ab8729dc5c251 \ - --hash=sha256:e570cfc40a29d6ad46c9aeaddbdcee687880940a3a327f2c668dd0e4ef0a441d \ - --hash=sha256:eb30cf008850c0a26b72bd1b9be6730830165ce049d239cfdccd906f2685f892 \ - --hash=sha256:f37fa70d95658763254941ddd30ecb23fc4ec0c5a788a7c21034fc2305dab7cc \ - --hash=sha256:f5ebeeec5c14533221eb30bad716bc1fd32f509196318fb9caa7002c4a364e4c \ - --hash=sha256:f5fa526d027d804b1f85cdda1eb091f70bde6fb7d87892f6dd5a48925bc88898 +sqlalchemy[asyncio]==2.0.20 \ + --hash=sha256:1506e988ebeaaf316f183da601f24eedd7452e163010ea63dbe52dc91c7fc70e \ + --hash=sha256:1a58052b5a93425f656675673ef1f7e005a3b72e3f2c91b8acca1b27ccadf5f4 \ + --hash=sha256:1b74eeafaa11372627ce94e4dc88a6751b2b4d263015b3523e2b1e57291102f0 \ + --hash=sha256:1be86ccea0c965a1e8cd6ccf6884b924c319fcc85765f16c69f1ae7148eba64b \ + --hash=sha256:1d35d49a972649b5080557c603110620a86aa11db350d7a7cb0f0a3f611948a0 \ + --hash=sha256:243d0fb261f80a26774829bc2cee71df3222587ac789b7eaf6555c5b15651eed \ + --hash=sha256:26a3399eaf65e9ab2690c07bd5cf898b639e76903e0abad096cd609233ce5208 \ + --hash=sha256:27d554ef5d12501898d88d255c54eef8414576f34672e02fe96d75908993cf53 \ + --hash=sha256:3364b7066b3c7f4437dd345d47271f1251e0cfb0aba67e785343cdbdb0fff08c \ + --hash=sha256:3423dc2a3b94125094897118b52bdf4d37daf142cbcf26d48af284b763ab90e9 \ + --hash=sha256:3c6aceebbc47db04f2d779db03afeaa2c73ea3f8dcd3987eb9efdb987ffa09a3 \ + --hash=sha256:3ce5e81b800a8afc870bb8e0a275d81957e16f8c4b62415a7b386f29a0cb9763 \ + --hash=sha256:411e7f140200c02c4b953b3dbd08351c9f9818d2bd591b56d0fa0716bd014f1e \ + --hash=sha256:4cde2e1096cbb3e62002efdb7050113aa5f01718035ba9f29f9d89c3758e7e4e \ + --hash=sha256:5768c268df78bacbde166b48be788b83dddaa2a5974b8810af422ddfe68a9bc8 \ + --hash=sha256:599ccd23a7146e126be1c7632d1d47847fa9f333104d03325c4e15440fc7d927 \ + --hash=sha256:5ed61e3463021763b853628aef8bc5d469fe12d95f82c74ef605049d810f3267 \ + --hash=sha256:63a368231c53c93e2b67d0c5556a9836fdcd383f7e3026a39602aad775b14acf \ + --hash=sha256:63e73da7fb030ae0a46a9ffbeef7e892f5def4baf8064786d040d45c1d6d1dc5 \ + --hash=sha256:6eb6d77c31e1bf4268b4d61b549c341cbff9842f8e115ba6904249c20cb78a61 \ + --hash=sha256:6f8a934f9dfdf762c844e5164046a9cea25fabbc9ec865c023fe7f300f11ca4a \ + --hash=sha256:6fe7d61dc71119e21ddb0094ee994418c12f68c61b3d263ebaae50ea8399c4d4 \ + --hash=sha256:759b51346aa388c2e606ee206c0bc6f15a5299f6174d1e10cadbe4530d3c7a98 \ + --hash=sha256:76fdfc0f6f5341987474ff48e7a66c3cd2b8a71ddda01fa82fedb180b961630a \ + --hash=sha256:77d37c1b4e64c926fa3de23e8244b964aab92963d0f74d98cbc0783a9e04f501 \ + --hash=sha256:79543f945be7a5ada9943d555cf9b1531cfea49241809dd1183701f94a748624 \ + --hash=sha256:79fde625a0a55220d3624e64101ed68a059c1c1f126c74f08a42097a72ff66a9 \ + --hash=sha256:7d3f175410a6db0ad96b10bfbb0a5530ecd4fcf1e2b5d83d968dd64791f810ed \ + --hash=sha256:8dd77fd6648b677d7742d2c3cc105a66e2681cc5e5fb247b88c7a7b78351cf74 \ + --hash=sha256:a3f0dd6d15b6dc8b28a838a5c48ced7455c3e1fb47b89da9c79cc2090b072a50 \ + --hash=sha256:bcb04441f370cbe6e37c2b8d79e4af9e4789f626c595899d94abebe8b38f9a4d \ + --hash=sha256:c3d99ba99007dab8233f635c32b5cd24fb1df8d64e17bc7df136cedbea427897 \ + --hash=sha256:ca8a5ff2aa7f3ade6c498aaafce25b1eaeabe4e42b73e25519183e4566a16fc6 \ + --hash=sha256:cb0d3e94c2a84215532d9bcf10229476ffd3b08f481c53754113b794afb62d14 \ + --hash=sha256:d1b09ba72e4e6d341bb5bdd3564f1cea6095d4c3632e45dc69375a1dbe4e26ec \ + --hash=sha256:d32b5ffef6c5bcb452723a496bad2d4c52b346240c59b3e6dba279f6dcc06c14 \ + --hash=sha256:d3793dcf5bc4d74ae1e9db15121250c2da476e1af8e45a1d9a52b1513a393459 \ + --hash=sha256:dd81466bdbc82b060c3c110b2937ab65ace41dfa7b18681fdfad2f37f27acdd7 \ + --hash=sha256:e4e571af672e1bb710b3cc1a9794b55bce1eae5aed41a608c0401885e3491179 \ + --hash=sha256:ea8186be85da6587456c9ddc7bf480ebad1a0e6dcbad3967c4821233a4d4df57 \ + --hash=sha256:eefebcc5c555803065128401a1e224a64607259b5eb907021bf9b175f315d2a6 # via # alembic - # sqlmodel -sqlalchemy2-stubs==0.0.2a35 \ - --hash=sha256:593784ff9fc0dc2ded1895e3322591689db3be06f3ca006e3ef47640baf2d38a \ - --hash=sha256:bd5d530697d7e8c8504c7fe792ef334538392a5fb7aa7e4f670bfacdd668a19d - # via sqlmodel -sqlmodel==0.0.8 \ - --hash=sha256:0fd805719e0c5d4f22be32eb3ffc856eca3f7f20e8c7aa3e117ad91684b518ee \ - --hash=sha256:3371b4d1ad59d2ffd0c530582c2140b6c06b090b32af9b9c6412986d7b117036 - # via - # fastapi-users-db-sqlmodel + # fastapi-users-db-sqlalchemy # zoo (pyproject.toml) starlette==0.27.0 \ --hash=sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75 \ @@ -509,7 +588,8 @@ typing-extensions==4.7.1 \ # alembic # fastapi # pydantic - # sqlalchemy2-stubs + # pydantic-core + # sqlalchemy uvicorn==0.23.2 \ --hash=sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53 \ --hash=sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a diff --git a/tests/backend/test_animals.py b/tests/backend/test_animals.py index 97c5ace..12a8445 100644 --- a/tests/backend/test_animals.py +++ b/tests/backend/test_animals.py @@ -6,7 +6,7 @@ from fastapi.testclient import TestClient -from zoo.models import Animals +from zoo.schemas.animals import AnimalsRead def test_get_animal(client: TestClient) -> None: @@ -16,5 +16,5 @@ def test_get_animal(client: TestClient) -> None: response = client.get("/animals") assert response.status_code == 200 response_data = response.json() - first_animal = Animals(**response_data[0]) - assert isinstance(first_animal.modified_at, datetime.datetime) + first_animal = AnimalsRead(**response_data[0]) + assert isinstance(first_animal.updated_at, datetime.datetime) diff --git a/tests/models/test_animals_models.py b/tests/models/test_animals_models.py index 4cde22e..bb779fc 100644 --- a/tests/models/test_animals_models.py +++ b/tests/models/test_animals_models.py @@ -5,7 +5,7 @@ import pytest from pydantic import ValidationError -from zoo.models.animals import AnimalsBase +from zoo.schemas.animals import AnimalsBase def test_animal_base_success(): diff --git a/zoo/__main__.py b/zoo/__main__.py new file mode 100644 index 0000000..9f27e1d --- /dev/null +++ b/zoo/__main__.py @@ -0,0 +1,65 @@ +""" +Zoo CLI +""" + +import json +import logging +import pathlib +from dataclasses import dataclass + +import click +import uvicorn + +from zoo._version import __application__, __version__ +from zoo.app import ZooFastAPI, app +from zoo.config import ZooSettings, app_config + +logger = logging.getLogger(__name__) + + +@dataclass +class ZooContext: + """ + Context Object Passed Around Application + """ + + app: ZooFastAPI + config: ZooSettings + + +@click.group(invoke_without_command=True, name=__application__) +@click.version_option(version=__version__, prog_name=__application__) +@click.option("-h", "--host", default="localhost", help="Host to listen on") +@click.option("-p", "--port", default=8000, help="Port to listen on", type=int) +@click.option("-d", "--debug", is_flag=True, help="Enable debug mode", default=False) +@click.pass_context +def cli(context: click.Context, host: str, port: int, *, debug: bool) -> None: + """ + Zoo CLI + """ + context.obj = ZooContext(app=app, config=app_config) + if context.obj.config.DEBUG is True or debug is True: + context.obj.config.DEBUG = True + logging.root.setLevel(logging.DEBUG) + if context.invoked_subcommand is None: + uvicorn.run(context.obj.app, host=host, port=port) + + +@cli.command() +@click.pass_obj +def openapi(context: ZooContext) -> None: + """ + Generate openapi.json + """ + openapi_body = context.app.openapi() + logger.debug(openapi_body) + json_file = pathlib.Path(__file__).parent.parent / "docs" / "openapi.json" + logger.info("Generating OpenAPI Spec: %s", json_file) + json_file.write_text(json.dumps(openapi_body, indent=2)) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + if not app_config.DOCKER: + app_config.rich_logging(loggers=[logging.getLogger()]) + cli() diff --git a/zoo/backend/__init__.py b/zoo/api/__init__.py similarity index 100% rename from zoo/backend/__init__.py rename to zoo/api/__init__.py diff --git a/zoo/backend/animals.py b/zoo/api/animals.py similarity index 68% rename from zoo/backend/animals.py rename to zoo/api/animals.py index a8619b4..45a0246 100644 --- a/zoo/backend/animals.py +++ b/zoo/api/animals.py @@ -3,16 +3,16 @@ """ import logging -from typing import List, Optional +from typing import List, Optional, Sequence from fastapi import APIRouter, Depends, Query -from sqlalchemy import func +from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession -from sqlmodel import select -from zoo.backend.utils import check_model +from zoo.api.utils import check_model from zoo.db import get_async_session -from zoo.models.animals import Animals, AnimalsCreate, AnimalsRead, AnimalsUpdate +from zoo.models.animals import Animals +from zoo.schemas.animals import AnimalsCreate, AnimalsRead, AnimalsUpdate logger = logging.getLogger(__name__) @@ -24,7 +24,7 @@ async def get_animals( offset: int = 0, limit: int = Query(default=100, le=100), session: AsyncSession = Depends(get_async_session), -) -> List[Animals]: +) -> List[AnimalsRead]: """ Get animals from the database """ @@ -35,62 +35,69 @@ async def get_animals( .offset(offset) .limit(limit) ) - animals: List[Animals] = result.scalars().all() - return animals + animals: Sequence[Animals] = result.scalars().all() + animals_models = [AnimalsRead.model_validate(animal) for animal in animals] + return animals_models @animals_router.post("/animals", response_model=AnimalsRead) async def create_animal( animal: AnimalsCreate, session: AsyncSession = Depends(get_async_session) -) -> Animals: +) -> AnimalsRead: """ Create a new animal in the database """ - new_animal = Animals.from_orm(animal) + new_animal = Animals(**animal.model_dump(exclude_unset=True)) session.add(new_animal) await session.commit() await session.refresh(new_animal) - return new_animal + new_animal_model = AnimalsRead.model_validate(new_animal) + return new_animal_model @animals_router.get("/animals/{animal_id}", response_model=AnimalsRead) -async def get_animal(animal_id: int, session: AsyncSession = Depends(get_async_session)) -> Animals: +async def get_animal( + animal_id: int, session: AsyncSession = Depends(get_async_session) +) -> AnimalsRead: """ Get an animal from the database """ animal: Optional[Animals] = await session.get(Animals, animal_id) animal = check_model(model_instance=animal, model_class=Animals, id=animal_id) - return animal + animal_model = AnimalsRead.model_validate(animal) + return animal_model @animals_router.delete("/animals/{animal_id}", response_model=AnimalsRead) async def delete_animal( animal_id: int, session: AsyncSession = Depends(get_async_session) -) -> Animals: +) -> AnimalsRead: """ Delete an animal from the database """ animal: Optional[Animals] = await session.get(Animals, animal_id) animal = check_model(model_instance=animal, model_class=Animals, id=animal_id) - animal.deleted_at = func.CURRENT_TIMESTAMP() # type: ignore[assignment, union-attr] + animal.deleted_at = func.current_timestamp() session.add(animal) await session.commit() await session.refresh(animal) - return animal + animal_model = AnimalsRead.model_validate(animal) + return animal_model @animals_router.patch("/animals/{animal_id}", response_model=AnimalsRead) async def update_animal( animal_id: int, animal: AnimalsUpdate, session: AsyncSession = Depends(get_async_session) -) -> Animals: +) -> AnimalsRead: """ Update an animal in the database """ db_animal: Optional[Animals] = await session.get(Animals, animal_id) db_animal = check_model(model_instance=db_animal, model_class=Animals, id=animal_id) - for field, value in animal.dict(exclude_unset=True).items(): + for field, value in animal.model_dump(exclude_unset=True).items(): setattr(db_animal, field, value) session.add(db_animal) await session.commit() await session.refresh(db_animal) - return db_animal + animal_model = AnimalsRead.model_validate(db_animal) + return animal_model diff --git a/zoo/backend/exhibits.py b/zoo/api/exhibits.py similarity index 71% rename from zoo/backend/exhibits.py rename to zoo/api/exhibits.py index 1e4de3c..4035a4b 100644 --- a/zoo/backend/exhibits.py +++ b/zoo/api/exhibits.py @@ -3,19 +3,21 @@ """ import logging -from typing import List, Optional +from typing import List, Optional, Sequence from fastapi import APIRouter, Depends, Query -from sqlalchemy import func +from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import joinedload -from sqlmodel import select -from zoo.backend.utils import check_model +from zoo.api.utils import check_model from zoo.db import get_async_session -from zoo.models.animals import Animals, AnimalsRead -from zoo.models.exhibits import Exhibits, ExhibitsCreate, ExhibitsRead, ExhibitsUpdate -from zoo.models.staff import Staff, StaffRead +from zoo.models.animals import Animals +from zoo.models.exhibits import Exhibits +from zoo.models.staff import Staff +from zoo.schemas.animals import AnimalsRead +from zoo.schemas.exhibits import ExhibitsCreate, ExhibitsRead, ExhibitsUpdate +from zoo.schemas.staff import StaffRead logger = logging.getLogger(__name__) @@ -27,7 +29,7 @@ async def get_exhibits( offset: int = 0, limit: int = Query(default=100, le=100), session: AsyncSession = Depends(get_async_session), -) -> List[Exhibits]: +) -> List[ExhibitsRead]: """ Get exhibits from the database """ @@ -38,77 +40,86 @@ async def get_exhibits( .offset(offset) .limit(limit) ) - exhibits: List[Exhibits] = result.scalars().all() - return exhibits + exhibits: Sequence[Exhibits] = result.scalars().all() + exhibit_models = [ExhibitsRead.model_validate(exhibit) for exhibit in exhibits] + return exhibit_models + + +class Exhibit: + pass @exhibits_router.post("/exhibits", response_model=ExhibitsRead) async def create_exhibit( exhibit: ExhibitsCreate, session: AsyncSession = Depends(get_async_session) -) -> Exhibits: +) -> ExhibitsRead: """ Create a new exhibit in the database """ - new_exhibit = Exhibits.from_orm(exhibit) + new_exhibit = Exhibit(**exhibit.model_dump(exclude_unset=True)) session.add(new_exhibit) await session.commit() await session.refresh(new_exhibit) - return new_exhibit + exhibit_model = ExhibitsRead.model_validate(new_exhibit) + return exhibit_model @exhibits_router.get("/exhibits/{exhibit_id}", response_model=ExhibitsRead) async def get_exhibit( exhibit_id: int, session: AsyncSession = Depends(get_async_session), -) -> Exhibits: +) -> ExhibitsRead: """ Get exhibit from the database """ exhibit: Optional[Exhibits] = await session.get(Exhibits, exhibit_id) exhibit = check_model(model_instance=exhibit, model_class=Exhibits, id=exhibit_id) - return exhibit + exhibit_model = ExhibitsRead.model_validate(exhibit) + return exhibit_model @exhibits_router.delete("/exhibits/{exhibit_id}", response_model=ExhibitsRead) async def delete_exhibit( exhibit_id: int, session: AsyncSession = Depends(get_async_session), -) -> Exhibits: +) -> ExhibitsRead: """ Delete exhibit from the database """ exhibit: Optional[Exhibits] = await session.get(Exhibits, exhibit_id) exhibit = check_model(model_instance=exhibit, model_class=Exhibits, id=exhibit_id) - exhibit.deleted_at = func.CURRENT_TIMESTAMP() # type: ignore[assignment, union-attr] + exhibit.deleted_at = func.current_timestamp() session.add(exhibit) await session.commit() await session.refresh(exhibit) - return exhibit + exhibit_model = ExhibitsRead.model_validate(exhibit) + return exhibit_model @exhibits_router.patch("/exhibits/{exhibit_id}", response_model=ExhibitsRead) async def update_exhibit( exhibit_id: int, exhibit: ExhibitsUpdate, session: AsyncSession = Depends(get_async_session) -) -> Exhibits: +) -> ExhibitsRead: """ Update exhibit from the database """ db_exhibit: Optional[Exhibits] = await session.get(Exhibits, exhibit_id) db_exhibit = check_model(model_instance=db_exhibit, model_class=Exhibits, id=exhibit_id) - for field, value in exhibit.dict(exclude_unset=True).items(): + for field, value in exhibit.model_dump(exclude_unset=True).items(): if value is not None: setattr(db_exhibit, field, value) session.add(db_exhibit) await session.commit() await session.refresh(db_exhibit) - return db_exhibit + exhibit_model = ExhibitsRead.model_validate(exhibit) + return exhibit_model @exhibits_router.get("/exhibits/{exhibit_id}/animals", response_model=List[AnimalsRead]) async def get_exhibit_animals( exhibit_id: int, session: AsyncSession = Depends(get_async_session), -) -> List[Animals]: +) -> List[AnimalsRead]: """ List animals in an exhibit """ @@ -121,14 +132,15 @@ async def get_exhibit_animals( ) exhibit = check_model(model_instance=exhibit, model_class=Exhibits, id=exhibit_id) animals: List[Animals] = exhibit.animals - return animals + animals_models = [AnimalsRead.model_validate(animal) for animal in animals] + return animals_models @exhibits_router.get("/exhibits/{exhibit_id}/staff", response_model=List[StaffRead]) async def get_exhibit_staff( exhibit_id: int, session: AsyncSession = Depends(get_async_session), -) -> List[Staff]: +) -> List[StaffRead]: """ List staff in an exhibit """ @@ -141,4 +153,5 @@ async def get_exhibit_staff( ) exhibit = check_model(model_instance=exhibit, model_class=Exhibits, id=exhibit_id) staff: List[Staff] = exhibit.staff - return staff + staff_models = [StaffRead.model_validate(staff) for staff in staff] + return staff_models diff --git a/zoo/backend/staff.py b/zoo/api/staff.py similarity index 68% rename from zoo/backend/staff.py rename to zoo/api/staff.py index 74c49eb..7862269 100644 --- a/zoo/backend/staff.py +++ b/zoo/api/staff.py @@ -3,16 +3,16 @@ """ import logging -from typing import List, Optional +from typing import List, Optional, Sequence from fastapi import APIRouter, Depends, Query -from sqlalchemy import func +from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession -from sqlmodel import select -from zoo.backend.utils import check_model +from zoo.api.utils import check_model from zoo.db import get_async_session -from zoo.models.staff import Staff, StaffCreate, StaffRead, StaffUpdate +from zoo.models.staff import Staff +from zoo.schemas.staff import StaffCreate, StaffRead, StaffUpdate logger = logging.getLogger(__name__) @@ -24,7 +24,7 @@ async def get_staff_members( offset: int = 0, limit: int = Query(default=100, le=100), session: AsyncSession = Depends(get_async_session), -) -> List[Staff]: +) -> List[StaffRead]: """ Get staff from the database """ @@ -35,61 +35,68 @@ async def get_staff_members( .offset(offset) .limit(limit) ) - staff: List[Staff] = result.scalars().all() - return staff + staff: Sequence[Staff] = result.scalars().all() + staff_models = [StaffRead.model_validate(staff_member) for staff_member in staff] + return staff_models @staff_router.get("/staff/{staff_id}", response_model=StaffRead) -async def get_staff(staff_id: int, session: AsyncSession = Depends(get_async_session)) -> Staff: +async def get_staff(staff_id: int, session: AsyncSession = Depends(get_async_session)) -> StaffRead: """ Get a staff from the database """ staff: Optional[Staff] = await session.get(Staff, staff_id) staff = check_model(model_instance=staff, model_class=Staff, id=staff_id) - return staff + staff_model = StaffRead.model_validate(staff) + return staff_model @staff_router.post("/staff", response_model=StaffRead) async def create_staff( staff: StaffCreate, session: AsyncSession = Depends(get_async_session) -) -> Staff: +) -> StaffRead: """ Create a new staff in the database """ - new_staff = Staff.from_orm(staff) + new_staff = Staff(**staff.model_dump(exclude_unset=True)) session.add(new_staff) await session.commit() await session.refresh(new_staff) - return new_staff + staff_model = StaffRead.model_validate(new_staff) + return staff_model @staff_router.delete("/staff/{staff_id}", response_model=StaffRead) -async def delete_staff(staff_id: int, session: AsyncSession = Depends(get_async_session)) -> Staff: +async def delete_staff( + staff_id: int, session: AsyncSession = Depends(get_async_session) +) -> StaffRead: """ Delete a staff in the database """ db_staff: Optional[Staff] = await session.get(Staff, staff_id) db_staff = check_model(model_instance=db_staff, model_class=Staff, id=staff_id) - db_staff.deleted_at = func.CURRENT_TIMESTAMP() # type: ignore[assignment, union-attr] + db_staff.deleted_at = func.current_timestamp() session.add(db_staff) await session.commit() await session.refresh(db_staff) - return db_staff + staff_model = StaffRead.model_validate(db_staff) + return staff_model @staff_router.patch("/staff/{staff_id}", response_model=StaffRead) async def update_staff( staff_id: int, staff: StaffUpdate, session: AsyncSession = Depends(get_async_session) -) -> Staff: +) -> StaffRead: """ Update a staff in the database """ db_staff: Optional[Staff] = await session.get(Staff, staff_id) db_staff = check_model(model_instance=db_staff, model_class=Staff, id=staff_id) - update_data = staff.dict(exclude_unset=True) + update_data = staff.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(db_staff, field, value) session.add(db_staff) await session.commit() await session.refresh(db_staff) - return db_staff + staff_model = StaffRead.model_validate(db_staff) + return staff_model diff --git a/zoo/backend/utils.py b/zoo/api/utils.py similarity index 83% rename from zoo/backend/utils.py rename to zoo/api/utils.py index 064521e..55db66d 100644 --- a/zoo/backend/utils.py +++ b/zoo/api/utils.py @@ -10,8 +10,8 @@ from starlette.responses import HTMLResponse from zoo._version import __application__, __favicon__ -from zoo.models.base import HasDeletedField -from zoo.models.utils import Health +from zoo.models.base import DatabaseTypeDeletedAt +from zoo.schemas.utils import Health utils_router = APIRouter(tags=["utilities"]) @@ -41,25 +41,25 @@ def swagger_docs() -> HTMLResponse: def check_model( - model_instance: Optional[HasDeletedField], - model_class: Type[HasDeletedField], + model_instance: Optional[DatabaseTypeDeletedAt], + model_class: Type[DatabaseTypeDeletedAt], id: int, # noqa: A002 -) -> HasDeletedField: +) -> DatabaseTypeDeletedAt: """ Handle a missing model Parameters ---------- - model_instance : Optional[HasDeletedField] + model_instance : Optional[DatabaseType] The model instance to check - model_class : Type[HasDeletedField] + model_class : Type[DatabaseType] The model class to check id : int The ID of the model instance Returns ------- - HasDeletedField + DatabaseType The model instance (if it exists) Raises diff --git a/zoo/app.py b/zoo/app.py index f7d0bf4..1af831b 100644 --- a/zoo/app.py +++ b/zoo/app.py @@ -2,24 +2,19 @@ zoo app """ -import argparse -import json -import pathlib -from typing import Any, Dict - import uvicorn from fastapi import FastAPI from zoo._version import __application__, __markdown_description__, __version__ -from zoo.backend.animals import animals_router -from zoo.backend.exhibits import exhibits_router -from zoo.backend.staff import staff_router -from zoo.backend.utils import utils_router -from zoo.config import config -from zoo.models.user import bootstrap_fastapi_users - -if not config.DOCKER: - config.rich_logging( +from zoo.api.animals import animals_router +from zoo.api.exhibits import exhibits_router +from zoo.api.staff import staff_router +from zoo.api.utils import utils_router +from zoo.config import app_config +from zoo.models.users import bootstrap_fastapi_users + +if not app_config.DOCKER: + app_config.rich_logging( loggers=[ "uvicorn", "uvicorn.access", @@ -32,14 +27,6 @@ class ZooFastAPI(FastAPI): Zoo FastAPI """ - def openapi(self) -> Dict[str, Any]: - """ - OpenAPI - """ - # if not self.openapi_schema: - raise ValueError(self.openapi_schema) - return self.openapi_schema - app = ZooFastAPI( title=__application__, @@ -48,30 +35,20 @@ def openapi(self) -> Dict[str, Any]: debug=False, docs_url=None, # Custom Swagger UI @ utils_router redoc_url=None, - generate_unique_id_function=config.custom_generate_unique_id, + generate_unique_id_function=app_config.custom_generate_unique_id, ) +# Routers app_routers = [ utils_router, animals_router, exhibits_router, staff_router, ] - for router in app_routers: app.include_router(router) -# FastAPI Users - add routers +# FastAPI Users Setup bootstrap_fastapi_users(app=app) if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "--openapi", action="store_true", help="Generate openapi.json", default=False - ) - args = parser.parse_args() - if args.openapi is True: - openapi_body = app.openapi() - json_file = pathlib.Path(__file__).parent.parent / "docs" / "openapi.json" - json_file.write_text(json.dumps(openapi_body, indent=2)) - else: - uvicorn.run(app) + uvicorn.run(app) diff --git a/zoo/config.py b/zoo/config.py index 28c0372..53cc2a8 100644 --- a/zoo/config.py +++ b/zoo/config.py @@ -11,7 +11,7 @@ import starlette import uvicorn from fastapi.routing import APIRoute -from pydantic import BaseSettings +from pydantic_settings import BaseSettings, SettingsConfigDict from rich.logging import RichHandler from sqlalchemy.engine import URL @@ -48,32 +48,28 @@ class ZooSettings(BaseSettings): DATABASE_USER: Optional[str] = None DATABASE_PASSWORD: Optional[str] = None DATABASE_NAME: Optional[str] = None + JWT_EXPIRATION: Optional[int] = None DATABASE_SECRET: str = __application__ - class Config: - """ - Pydantic configuration - """ - - env_prefix = "ZOO_" - case_sensitive = True + model_config = SettingsConfigDict( + env_prefix="ZOO_", + case_sensitive=True, + ) @property def connection_string(self) -> str: """ Get the database connection string """ - database_url = str( - URL.create( - drivername=self.DATABASE_DRIVER, - username=self.DATABASE_USER, - password=self.DATABASE_PASSWORD, - host=self.DATABASE_HOST or self.DATABASE_FILE, - port=self.DATABASE_PORT, - database=self.DATABASE_NAME, - ) - ) + database_url = URL.create( + drivername=self.DATABASE_DRIVER, + username=self.DATABASE_USER, + password=self.DATABASE_PASSWORD, + host=self.DATABASE_HOST or self.DATABASE_FILE, + port=self.DATABASE_PORT, + database=self.DATABASE_NAME, + ).render_as_string(hide_password=False) if all( [ self.DATABASE_HOST is None, @@ -104,4 +100,4 @@ def custom_generate_unique_id(cls, route: APIRoute): return f"{route.tags[0]}-{route.name}" -config = ZooSettings() +app_config = ZooSettings() diff --git a/zoo/db.py b/zoo/db.py index 6719c6c..3949c0d 100644 --- a/zoo/db.py +++ b/zoo/db.py @@ -4,14 +4,12 @@ from typing import AsyncGenerator -from sqlalchemy.ext.asyncio import create_async_engine -from sqlalchemy.orm import sessionmaker -from sqlmodel.ext.asyncio.session import AsyncSession +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine -from zoo.config import config +from zoo.config import app_config -engine = create_async_engine(config.connection_string, echo=config.DEBUG, future=True) -async_session = sessionmaker( +engine = create_async_engine(app_config.connection_string, echo=app_config.DEBUG, future=True) +async_session = async_sessionmaker( engine, class_=AsyncSession, autocommit=False, expire_on_commit=False, autoflush=False ) diff --git a/zoo/models/__init__.py b/zoo/models/__init__.py index b24007a..9dd21b1 100644 --- a/zoo/models/__init__.py +++ b/zoo/models/__init__.py @@ -7,7 +7,7 @@ from zoo.models.animals import Animals from zoo.models.exhibits import Exhibits from zoo.models.staff import Staff -from zoo.models.user import AccessToken, User +from zoo.models.users import AccessToken, User __all_models__ = [Animals, Exhibits, Staff, User, AccessToken] diff --git a/zoo/models/animals.py b/zoo/models/animals.py index 8eb2777..c197065 100644 --- a/zoo/models/animals.py +++ b/zoo/models/animals.py @@ -1,100 +1,27 @@ """ -Animal models +Animals Database Model """ +from typing import TYPE_CHECKING -from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlmodel import Field, Relationship - -from zoo.models.base import ( - CreatedModifiedMixin, - DeletedMixin, - OptionalIdMixin, - RequiredIdMixin, - ZooModel, -) +from zoo.models.base import Base, CreatedUpdatedMixin, DeletedAtMixin, IDMixin if TYPE_CHECKING: from zoo.models.exhibits import Exhibits -class AnimalsBase(ZooModel): - """ - Animals model base - """ - - name: str = Field(description="The name of the animal", index=True) - description: Optional[str] = Field(default=None, description="The description of the animal") - species: Optional[str] = Field(default=None, description="The species of the animal") - - exhibit_id: Optional[int] = Field( - description="The id of the exhibit", foreign_key="exhibits.id", nullable=True, default=None - ) - - __example__: ClassVar[Dict[str, Any]] = { - "name": "Lion", - "description": "Ferocious kitty", - "species": "Panthera leo", - "exhibit_id": 1, - } - - -class AnimalsCreate(AnimalsBase): - """ - Animals model: create - """ - - class Config: - """ - Config for AnimalCreate - """ - - schema_extra: ClassVar[Dict[str, Any]] = AnimalsBase.get_openapi_create_example() - - -class AnimalsRead( - DeletedMixin, - CreatedModifiedMixin, - AnimalsBase, - RequiredIdMixin, -): - """ - Animals model: read - """ - - class Config: - """ - Config for AnimalRead - """ - - schema_extra: ClassVar[Dict[str, Any]] = AnimalsBase.get_openapi_read_example() - - -class Animals(DeletedMixin, CreatedModifiedMixin, AnimalsBase, OptionalIdMixin, table=True): - """ - Animals model: database table - """ - - exhibit: "Exhibits" = Relationship(back_populates="animals") - - -class AnimalsUpdate(ZooModel): +class Animals(IDMixin, CreatedUpdatedMixin, DeletedAtMixin, Base): """ - Animals model: update + Animals Database Model """ - name: Optional[str] = Field( - default=None, description="The name of the animal", index=True, unique=True - ) - description: Optional[str] = Field(default=None, description="The description of the animal") - species: Optional[str] = Field(default=None, description="The species of the animal") - exhibit_id: Optional[int] = Field( - description="The id of the exhibit", foreign_key="exhibits.id", nullable=True, default=None - ) + __tablename__ = "animals" - class Config: - """ - Config for AnimalUpdate - """ + name: Mapped[str] + description: Mapped[str] = mapped_column(default=None, nullable=True) + species: Mapped[str] = mapped_column(default=None, nullable=True) + exhibit_id: Mapped[int] = mapped_column(ForeignKey("exhibits.id"), nullable=True, default=None) - schema_extra: ClassVar[Dict[str, Any]] = AnimalsBase.get_openapi_update_example() + exhibit: Mapped["Exhibits"] = relationship(back_populates="animals") diff --git a/zoo/models/base.py b/zoo/models/base.py index f9a311e..340d3e7 100644 --- a/zoo/models/base.py +++ b/zoo/models/base.py @@ -1,136 +1,63 @@ """ -Base Inheritance Models +SQLAlchemy Base Model """ import datetime -from typing import Any, ClassVar, Dict, Optional, TypeVar +from typing import TypeVar -from sqlalchemy import Column, DateTime, Table -from sqlmodel import Field, SQLModel, func +from sqlalchemy import DateTime, func +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column -class ZooModel(SQLModel): +class Base(DeclarativeBase): """ - Base model for all zoo models + SQLAlchemy Base Model """ - __table__: Table - - __example__: ClassVar[Dict[str, Any]] = {} - - __openapi_db_fields__: ClassVar[Dict[str, Any]] = { - "id": 1, - "created_at": "2021-01-01T00:00:00.000000", - "modified_at": "2021-01-02T09:12:34.567890", - "deleted_at": None, - } - - @classmethod - def get_openapi_read_example(cls) -> Dict[str, Any]: - """ - Get the openapi read example - """ - if not cls.__example__: - error_msg = "Example not implemented" - raise NotImplementedError(error_msg) - return { - "examples": [ - { - **cls.__example__, - **cls.__openapi_db_fields__, - } - ] - } - - @classmethod - def get_openapi_create_example(cls) -> Dict[str, Any]: - """ - Get the openapi create example - """ - if not cls.__example__: - error_msg = "Example not implemented" - raise NotImplementedError(error_msg) - return {"examples": [cls.__example__]} - - @classmethod - def get_openapi_update_example(cls) -> Dict[str, Any]: - """ - Get the openapi update example - """ - if not cls.__example__: - error_msg = "Example not implemented" - raise NotImplementedError(error_msg) - half_example_keys = list(cls.__example__.keys())[: len(cls.__example__) // 2] - half_example = {key: cls.__example__[key] for key in half_example_keys} - return {"examples": [half_example]} - - @classmethod - def get_openapi_delete_example(cls) -> Dict[str, Any]: - """ - Get the openapi delete example - """ - read_example = cls.get_openapi_read_example() - read_example["deleted_at"] = "2021-01-02T09:12:34.567890" - return read_example - - -class OptionalIdMixin(ZooModel): - """ - Id mixin, id is optional - This is used for create table models +class IDMixin: + """ + ID Mixin """ - id: Optional[int] = Field( # noqa: A003 - default=None, primary_key=True, description="The unique identifier for the table" - ) + id: Mapped[int] = mapped_column(primary_key=True) # noqa: A003 -class RequiredIdMixin(ZooModel): +class CreatedAtMixin: """ - Id mixin with required id + Created Updated Mixin """ - id: int = Field( # noqa: A003 - primary_key=True, description="The unique identifier for the table" + created_at: Mapped[datetime.datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now() ) -class CreatedModifiedMixin(ZooModel): +class UpdatedAtMixin: """ - Created and modified mixin + Created Updated Mixin """ - created_at: datetime.datetime = Field( - default_factory=datetime.datetime.utcnow, - nullable=False, - description="The date and time the record was created", - sa_column=Column(DateTime(timezone=True), server_default=func.CURRENT_TIMESTAMP()), - ) - modified_at: datetime.datetime = Field( - default_factory=datetime.datetime.utcnow, - nullable=False, - description="The date and time the record was last modified", - sa_column=Column( - DateTime(timezone=True), - server_default=func.CURRENT_TIMESTAMP(), - onupdate=func.CURRENT_TIMESTAMP(), - ), + updated_at: Mapped[datetime.datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() ) -class DeletedMixin(ZooModel): +class CreatedUpdatedMixin(CreatedAtMixin, UpdatedAtMixin): + """ + Created Updated Mixin + """ + + +class DeletedAtMixin: """ - Deleted mixin + Deleted At Mixin """ - deleted_at: Optional[datetime.datetime] = Field( - default=None, - nullable=True, - description="The date and time the record was deleted", - sa_column=Column(DateTime(timezone=True), default=None, nullable=True), + deleted_at: Mapped[datetime.datetime] = mapped_column( + DateTime(timezone=True), default=None, nullable=True ) -ZooModelType = TypeVar("ZooModelType", bound=ZooModel) -HasDeletedField = TypeVar("HasDeletedField", bound=DeletedMixin) +DatabaseType = TypeVar("DatabaseType", bound=Base) +DatabaseTypeDeletedAt = TypeVar("DatabaseTypeDeletedAt", bound=DeletedAtMixin) diff --git a/zoo/models/exhibits.py b/zoo/models/exhibits.py index 37d8f9e..5f5dfcc 100644 --- a/zoo/models/exhibits.py +++ b/zoo/models/exhibits.py @@ -1,98 +1,28 @@ """ -Exhibits models +Exhibits Database Model """ -from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Optional +from typing import TYPE_CHECKING, List -from sqlmodel import Field, Relationship +from sqlalchemy.orm import Mapped, mapped_column, relationship -from zoo.models.base import ( - CreatedModifiedMixin, - DeletedMixin, - OptionalIdMixin, - RequiredIdMixin, - ZooModel, -) +from zoo.models.base import Base, CreatedUpdatedMixin, DeletedAtMixin, IDMixin if TYPE_CHECKING: from zoo.models.animals import Animals from zoo.models.staff import Staff -class ExhibitsBase(ZooModel): +class Exhibits(IDMixin, CreatedUpdatedMixin, DeletedAtMixin, Base): """ - Exhibits model base + Exhibits Database Model """ - name: str = Field(description="The name of the exhibit", index=True, unique=True) - description: Optional[str] = Field(default=None, description="The description of the exhibit") - location: Optional[str] = Field(default=None, description="The location of the exhibit") + __tablename__ = "exhibits" - __example__: ClassVar[Dict[str, Any]] = { - "name": "Big Cat Exhibit", - "description": "A big cat exhibit", - "location": "North America", - } + name: Mapped[str] + description: Mapped[str] = mapped_column(default=None, nullable=True) + location: Mapped[str] = mapped_column(default=None, nullable=True) - -class ExhibitsCreate(ExhibitsBase): - """ - Exhibits model: create - """ - - class Config: - """ - Config for ExhibitCreate - """ - - schema_extra: ClassVar[Dict[str, Any]] = ExhibitsBase.get_openapi_create_example() - - -class ExhibitsRead( - DeletedMixin, - CreatedModifiedMixin, - ExhibitsBase, - RequiredIdMixin, -): - """ - Exhibits model: read - """ - - class Config: - """ - Config for ExhibitRead - """ - - schema_extra: ClassVar[Dict[str, Any]] = ExhibitsBase.get_openapi_read_example() - - -class Exhibits( - DeletedMixin, - CreatedModifiedMixin, - ExhibitsBase, - OptionalIdMixin, - table=True, -): - """ - Exhibits model: table - """ - - animals: List["Animals"] = Relationship(back_populates="exhibit") - staff: List["Staff"] = Relationship(back_populates="exhibit") - - -class ExhibitsUpdate(ZooModel): - """ - Exhibits model: update - """ - - name: Optional[str] = Field(default=None, description="The name of the exhibit", index=True) - description: Optional[str] = Field(default=None, description="The description of the exhibit") - location: Optional[str] = Field(default=None, description="The location of the exhibit") - - class Config: - """ - Config for ExhibitUpdate - """ - - schema_extra: ClassVar[Dict[str, Any]] = ExhibitsBase.get_openapi_update_example() + animals: Mapped[List["Animals"]] = relationship(back_populates="exhibit") + staff: Mapped[List["Staff"]] = relationship(back_populates="exhibit") diff --git a/zoo/models/staff.py b/zoo/models/staff.py index b328665..c78df44 100644 --- a/zoo/models/staff.py +++ b/zoo/models/staff.py @@ -1,114 +1,31 @@ """ -Exhibits models +Staff Database Model """ -from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional +from typing import TYPE_CHECKING -from pydantic import EmailStr -from sqlmodel import Field, Relationship +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship -from zoo.models.base import ( - CreatedModifiedMixin, - DeletedMixin, - OptionalIdMixin, - RequiredIdMixin, - ZooModel, -) +from zoo.models.base import Base, CreatedUpdatedMixin, DeletedAtMixin, IDMixin if TYPE_CHECKING: from zoo.models.exhibits import Exhibits -class StaffBase(ZooModel): +class Staff(Base, IDMixin, DeletedAtMixin, CreatedUpdatedMixin): """ - Staff model base + Staff Database Model """ - name: str = Field(description="The name of the staff", index=True, unique=True) - job_title: Optional[str] = Field(default=None, description="The job title of the staff") - email: Optional[EmailStr] = Field(default=None, description="The email of the staff") - phone: Optional[str] = Field(default=None, description="The phone number of the staff") - notes: Optional[str] = Field( - default=None, description="Optional notes regarding the staff member" - ) - exhibit_id: Optional[int] = Field( - description="The id of the exhibit", foreign_key="exhibits.id", nullable=True, default=None - ) + __tablename__ = "staff" - __example__: ClassVar[Dict[str, Any]] = { - "name": "John Doe", - "job_title": "Zookeeper", - "email": "big.cat.lover@gmail.com", - "phone": "555-555-5555", - "notes": "John Doe is a great zookeeper and loves cats!", - "exhibit_id": 1, - } + id: Mapped[int] = mapped_column(primary_key=True) # noqa: A003 + name: Mapped[str] + job_title: Mapped[str] = mapped_column(default=None, nullable=True) + email: Mapped[str] = mapped_column(default=None, nullable=True) + phone: Mapped[str] = mapped_column(default=None, nullable=True) + notes: Mapped[str] = mapped_column(default=None, nullable=True) + exhibit_id: Mapped[int] = mapped_column(ForeignKey("exhibits.id"), nullable=True, default=None) - -class StaffCreate(StaffBase): - """ - Staff model: create - """ - - class Config: - """ - Config for StaffCreate - """ - - schema_extra: ClassVar[Dict[str, Any]] = StaffBase.get_openapi_create_example() - - -class StaffRead( - DeletedMixin, - CreatedModifiedMixin, - StaffBase, - RequiredIdMixin, -): - """ - Staff model: read - """ - - class Config: - """ - Config for StaffRead - """ - - schema_extra: ClassVar[Dict[str, Any]] = StaffBase.get_openapi_read_example() - - -class Staff( - DeletedMixin, - CreatedModifiedMixin, - StaffBase, - OptionalIdMixin, - table=True, -): - """ - Staff model: database table - """ - - exhibit: Optional["Exhibits"] = Relationship(back_populates="staff") - - -class StaffUpdate(ZooModel): - """ - Staff model: update - """ - - name: Optional[str] = Field(description="The name of the staff") - job_title: Optional[str] = Field(default=None, description="The job title of the staff") - email: Optional[EmailStr] = Field(default=None, description="The email of the staff") - phone: Optional[str] = Field(default=None, description="The phone number of the staff") - notes: Optional[str] = Field( - default=None, description="Optional notes regarding the staff member" - ) - exhibit_id: Optional[int] = Field( - description="The id of the exhibit", foreign_key="exhibits.id", nullable=True, default=None - ) - - class Config: - """ - Config for StaffUpdate - """ - - schema_extra: ClassVar[Dict[str, Any]] = StaffBase.get_openapi_update_example() + exhibit: Mapped["Exhibits"] = relationship(back_populates="staff") diff --git a/zoo/models/user.py b/zoo/models/users.py similarity index 58% rename from zoo/models/user.py rename to zoo/models/users.py index 6fb7d76..7f1bb2b 100644 --- a/zoo/models/user.py +++ b/zoo/models/users.py @@ -1,56 +1,39 @@ """ -FastAPI Users +User Database Model """ import asyncio import contextlib -import logging import uuid +from typing import AsyncGenerator from fastapi import Depends, FastAPI -from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, schemas +from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin from fastapi_users.authentication import AuthenticationBackend, BearerTransport from fastapi_users.authentication.strategy import AccessTokenDatabase, DatabaseStrategy -from fastapi_users_db_sqlmodel import SQLModelBaseUserDB, SQLModelUserDatabaseAsync -from fastapi_users_db_sqlmodel.access_token import ( - SQLModelAccessTokenDatabaseAsync, - SQLModelBaseAccessToken, +from fastapi_users_db_sqlalchemy import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase +from fastapi_users_db_sqlalchemy.access_token import ( + SQLAlchemyAccessTokenDatabase, + SQLAlchemyBaseAccessTokenTableUUID, ) -from pydantic import EmailStr from sqlalchemy.ext.asyncio import AsyncSession -from zoo.config import config +from zoo.config import app_config from zoo.db import get_async_session -from zoo.models.base import CreatedModifiedMixin +from zoo.models.base import Base, CreatedUpdatedMixin, UpdatedAtMixin +from zoo.schemas.users import UserCreate, UserRead -logger = logging.getLogger(__name__) +auth_endpoint = "auth" +jwt_endpoint = "jwt" -class User(SQLModelBaseUserDB, CreatedModifiedMixin, table=True): +class User(SQLAlchemyBaseUserTableUUID, CreatedUpdatedMixin, Base): """ FastAPI Users - User Model """ -class UserRead(schemas.BaseUser[uuid.UUID], CreatedModifiedMixin): # type: ignore[misc] - """ - FastAPI Users - User Read Model - """ - - -class UserCreate(schemas.BaseUserCreate): - """ - FastAPI Users - User Create Model - """ - - -class UserUpdate(schemas.BaseUserUpdate): - """ - FastAPI Users - User Update Model - """ - - -class AccessToken(SQLModelBaseAccessToken, table=True): +class AccessToken(SQLAlchemyBaseAccessTokenTableUUID, UpdatedAtMixin, Base): """ FastAPI Users - Access Token Model """ @@ -63,26 +46,26 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): UserManager for FastAPI Users """ - reset_password_token_secret = config.DATABASE_SECRET - verification_token_secret = config.DATABASE_SECRET - async def get_user_db(session: AsyncSession = Depends(get_async_session)): """ Yield a SQLModelUserDatabaseAsync """ - yield SQLModelUserDatabaseAsync(user_model=User, session=session) + yield SQLAlchemyUserDatabase(user_table=User, session=session) async def get_access_token_db( session: AsyncSession = Depends(get_async_session), -): - yield SQLModelAccessTokenDatabaseAsync[AccessToken]( - access_token_model=AccessToken, session=session +) -> AsyncGenerator[SQLAlchemyAccessTokenDatabase[AccessToken], None]: + """ + Yield a SQLAlchemyAccessTokenDatabase + """ + yield SQLAlchemyAccessTokenDatabase[AccessToken]( + access_token_table=AccessToken, session=session ) -async def get_user_manager(user_db=Depends(get_user_db)): +async def get_user_manager(user_db=Depends(get_user_db)) -> AsyncGenerator[UserManager, None]: """ Yield a UserManager """ @@ -95,10 +78,10 @@ def get_database_strategy( """ Get a DatabaseStrategy using the AccessTokenDatabase """ - return DatabaseStrategy(database=access_token_db, lifetime_seconds=3600) + return DatabaseStrategy(database=access_token_db, lifetime_seconds=app_config.JWT_EXPIRATION) -bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") +bearer_transport = BearerTransport(tokenUrl=f"{auth_endpoint}/{jwt_endpoint}/login") auth_backend = AuthenticationBackend( name="jwt", transport=bearer_transport, @@ -117,9 +100,7 @@ def get_database_strategy( get_user_manager_context = contextlib.asynccontextmanager(get_user_manager) -async def create_user( - email: str, password: str, is_superuser: bool = False # noqa: FBT001, FBT002 -) -> User: +async def create_user(email: str, password: str, *, is_superuser: bool = False) -> User: """ Create a user @@ -145,7 +126,7 @@ async def create_user( async with get_user_db_context(session) as user_db: async with get_user_manager_context(user_db) as user_manager: user = await user_manager.create( - UserCreate(email=EmailStr(email), password=password, is_superuser=is_superuser) + UserCreate(email=email, password=password, is_superuser=is_superuser) ) return user @@ -163,28 +144,19 @@ def bootstrap_fastapi_users(app: FastAPI) -> None: None """ app.include_router( - fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"] + fastapi_users.get_auth_router(auth_backend), + prefix=f"/{auth_endpoint}/{jwt_endpoint}", + tags=[auth_endpoint], ) app.include_router( fastapi_users.get_register_router(UserRead, UserCreate), - prefix="/auth", - tags=["auth"], - ) - app.include_router( - fastapi_users.get_reset_password_router(), - prefix="/auth", - tags=["auth"], - ) - app.include_router( - fastapi_users.get_verify_router(UserRead), - prefix="/auth", - tags=["auth"], - ) - app.include_router( - fastapi_users.get_users_router(UserRead, UserUpdate), - prefix="/users", - tags=["users"], + prefix=f"/{auth_endpoint}", + tags=[auth_endpoint], ) + # Skipping Routers: + # - fastapi_users.get_reset_password_router() + # - fastapi_users.get_verify_router(UserRead) + # - fastapi_users.get_users_router(UserRead, UserUpdate) if __name__ == "__main__": diff --git a/zoo/schemas/__init__.py b/zoo/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zoo/schemas/animals.py b/zoo/schemas/animals.py new file mode 100644 index 0000000..2124088 --- /dev/null +++ b/zoo/schemas/animals.py @@ -0,0 +1,70 @@ +""" +Animal models +""" + +from typing import Any, ClassVar, Dict, Optional + +from pydantic import ConfigDict, Field + +from zoo.schemas.base import ( + CreatedModifiedMixin, + DeletedMixin, + RequiredIdMixin, + ZooModel, +) + + +class AnimalsBase(ZooModel): + """ + Animals model base + """ + + name: str = Field(description="The name of the animal") + description: Optional[str] = Field(default=None, description="The description of the animal") + species: Optional[str] = Field(default=None, description="The species of the animal") + + exhibit_id: Optional[int] = Field(description="The id of the exhibit", default=None) + + __example__: ClassVar[Dict[str, Any]] = { + "name": "Lion", + "description": "Ferocious kitty", + "species": "Panthera leo", + "exhibit_id": 1, + } + + +class AnimalsCreate(AnimalsBase): + """ + Animals model: create + """ + + model_config = ConfigDict(json_schema_extra=AnimalsBase.get_openapi_create_example()) + + +class AnimalsRead( + DeletedMixin, + CreatedModifiedMixin, + AnimalsBase, + RequiredIdMixin, +): + """ + Animals model: read + """ + + model_config = ConfigDict( + from_attributes=True, + json_schema_extra=AnimalsBase.get_openapi_read_example(), + ) + + +class AnimalsUpdate(ZooModel): + """ + Animals model: update + """ + + name: Optional[str] = Field(default=None, description="The name of the animal") + description: Optional[str] = Field(default=None, description="The description of the animal") + species: Optional[str] = Field(default=None, description="The species of the animal") + exhibit_id: Optional[int] = Field(description="The id of the exhibit", default=None) + + model_config = ConfigDict(json_schema_extra=AnimalsBase.get_openapi_update_example()) diff --git a/zoo/schemas/base.py b/zoo/schemas/base.py new file mode 100644 index 0000000..d43bbcb --- /dev/null +++ b/zoo/schemas/base.py @@ -0,0 +1,119 @@ +""" +Base Inheritance Models +""" + +import datetime +from typing import Any, ClassVar, Dict, Optional, TypeVar + +from pydantic import BaseModel, Field + + +class ZooModel(BaseModel): + """ + Base model for all zoo models + """ + + __example__: ClassVar[Dict[str, Any]] = {} + + __openapi_db_fields__: ClassVar[Dict[str, Any]] = { + "id": 1, + "created_at": "2021-01-01T00:00:00.000000", + "modified_at": "2021-01-02T09:12:34.567890", + "deleted_at": None, + } + + @classmethod + def get_openapi_read_example(cls) -> Dict[str, Any]: + """ + Get the openapi read example + """ + if not cls.__example__: + error_msg = "Example not implemented" + raise NotImplementedError(error_msg) + return { + "examples": [ + { + **cls.__example__, + **cls.__openapi_db_fields__, + } + ] + } + + @classmethod + def get_openapi_create_example(cls) -> Dict[str, Any]: + """ + Get the openapi create example + """ + if not cls.__example__: + error_msg = "Example not implemented" + raise NotImplementedError(error_msg) + return {"examples": [cls.__example__]} + + @classmethod + def get_openapi_update_example(cls) -> Dict[str, Any]: + """ + Get the openapi update example + """ + if not cls.__example__: + error_msg = "Example not implemented" + raise NotImplementedError(error_msg) + half_example_keys = list(cls.__example__.keys())[: len(cls.__example__) // 2] + half_example = {key: cls.__example__[key] for key in half_example_keys} + return {"examples": [half_example]} + + @classmethod + def get_openapi_delete_example(cls) -> Dict[str, Any]: + """ + Get the openapi delete example + """ + read_example = cls.get_openapi_read_example() + read_example["deleted_at"] = "2021-01-02T09:12:34.567890" + return read_example + + +class OptionalIdMixin(ZooModel): + """ + Id mixin, id is optional + + This is used for create table models + """ + + id: Optional[int] = Field( # noqa: A003 + default=None, description="The unique identifier for the table" + ) + + +class RequiredIdMixin(ZooModel): + """ + Id mixin with required id + """ + + id: int = Field(description="The unique identifier for the table") # noqa: A003 + + +class CreatedModifiedMixin(ZooModel): + """ + Created and modified mixin + """ + + created_at: datetime.datetime = Field( + description="The date and time the record was created", + ) + updated_at: datetime.datetime = Field( + description="The date and time the record was last modified", + ) + + +class DeletedMixin(ZooModel): + """ + Deleted mixin + """ + + deleted_at: Optional[datetime.datetime] = Field( + default=None, + description="The date and time the record was deleted", + ) + + +ZooModelType = TypeVar("ZooModelType", bound=ZooModel) +HasDeletedField = TypeVar("HasDeletedField", bound=DeletedMixin) diff --git a/zoo/schemas/exhibits.py b/zoo/schemas/exhibits.py new file mode 100644 index 0000000..5e2edcf --- /dev/null +++ b/zoo/schemas/exhibits.py @@ -0,0 +1,65 @@ +""" +Exhibits models +""" + +from typing import Any, ClassVar, Dict, Optional + +from pydantic import ConfigDict, Field + +from zoo.schemas.base import ( + CreatedModifiedMixin, + DeletedMixin, + RequiredIdMixin, + ZooModel, +) + + +class ExhibitsBase(ZooModel): + """ + Exhibits model base + """ + + name: str = Field(description="The name of the exhibit") + description: Optional[str] = Field(default=None, description="The description of the exhibit") + location: Optional[str] = Field(default=None, description="The location of the exhibit") + + __example__: ClassVar[Dict[str, Any]] = { + "name": "Big Cat Exhibit", + "description": "A big cat exhibit", + "location": "North America", + } + + +class ExhibitsCreate(ExhibitsBase): + """ + Exhibits model: create + """ + + model_config = ConfigDict(json_schema_extra=ExhibitsBase.get_openapi_create_example()) + + +class ExhibitsRead( + DeletedMixin, + CreatedModifiedMixin, + ExhibitsBase, + RequiredIdMixin, +): + """ + Exhibits model: read + """ + + model_config = ConfigDict( + from_attributes=True, json_schema_extra=ExhibitsBase.get_openapi_read_example() + ) + + +class ExhibitsUpdate(ZooModel): + """ + Exhibits model: update + """ + + name: Optional[str] = Field(default=None, description="The name of the exhibit") + description: Optional[str] = Field(default=None, description="The description of the exhibit") + location: Optional[str] = Field(default=None, description="The location of the exhibit") + + model_config = ConfigDict(json_schema_extra=ExhibitsBase.get_openapi_update_example()) diff --git a/zoo/schemas/staff.py b/zoo/schemas/staff.py new file mode 100644 index 0000000..98128c2 --- /dev/null +++ b/zoo/schemas/staff.py @@ -0,0 +1,78 @@ +""" +Exhibits models +""" + +from typing import Any, ClassVar, Dict, Optional + +from pydantic import ConfigDict, EmailStr, Field + +from zoo.schemas.base import ( + CreatedModifiedMixin, + DeletedMixin, + RequiredIdMixin, + ZooModel, +) + + +class StaffBase(ZooModel): + """ + Staff model base + """ + + name: str = Field(description="The name of the staff") + job_title: Optional[str] = Field(default=None, description="The job title of the staff") + email: Optional[EmailStr] = Field(default=None, description="The email of the staff") + phone: Optional[str] = Field(default=None, description="The phone number of the staff") + notes: Optional[str] = Field( + default=None, description="Optional notes regarding the staff member" + ) + exhibit_id: Optional[int] = Field(description="The id of the exhibit", default=None) + + __example__: ClassVar[Dict[str, Any]] = { + "name": "John Doe", + "job_title": "Zookeeper", + "email": "big.cat.lover@gmail.com", + "phone": "555-555-5555", + "notes": "John Doe is a great zookeeper and loves cats!", + "exhibit_id": 1, + } + + +class StaffCreate(StaffBase): + """ + Staff model: create + """ + + model_config = ConfigDict(json_schema_extra=StaffBase.get_openapi_create_example()) + + +class StaffRead( + DeletedMixin, + CreatedModifiedMixin, + StaffBase, + RequiredIdMixin, +): + """ + Staff model: read + """ + + model_config = ConfigDict( + json_schema_extra=StaffBase.get_openapi_read_example(), from_attributes=True + ) + + +class StaffUpdate(ZooModel): + """ + Staff model: update + """ + + name: Optional[str] = Field(description="The name of the staff") + job_title: Optional[str] = Field(default=None, description="The job title of the staff") + email: Optional[EmailStr] = Field(default=None, description="The email of the staff") + phone: Optional[str] = Field(default=None, description="The phone number of the staff") + notes: Optional[str] = Field( + default=None, description="Optional notes regarding the staff member" + ) + exhibit_id: Optional[int] = Field(description="The id of the exhibit", default=None) + + model_config = ConfigDict(json_schema_extra=StaffBase.get_openapi_update_example()) diff --git a/zoo/schemas/users.py b/zoo/schemas/users.py new file mode 100644 index 0000000..3a277a7 --- /dev/null +++ b/zoo/schemas/users.py @@ -0,0 +1,30 @@ +""" +FastAPI Users +""" + +import logging +import uuid + +from fastapi_users import schemas + +from zoo.schemas.base import CreatedModifiedMixin + +logger = logging.getLogger(__name__) + + +class UserRead(schemas.BaseUser[uuid.UUID], CreatedModifiedMixin): + """ + FastAPI Users - User Read Model + """ + + +class UserCreate(schemas.BaseUserCreate): + """ + FastAPI Users - User Create Model + """ + + +class UserUpdate(schemas.BaseUserUpdate): + """ + FastAPI Users - User Update Model + """ diff --git a/zoo/models/utils.py b/zoo/schemas/utils.py similarity index 72% rename from zoo/models/utils.py rename to zoo/schemas/utils.py index a46785e..14725b6 100644 --- a/zoo/models/utils.py +++ b/zoo/schemas/utils.py @@ -3,12 +3,13 @@ """ import datetime -from typing import Any, ClassVar, Dict -from sqlmodel import Field, SQLModel +from pydantic import ConfigDict, Field +from zoo.schemas.base import ZooModel -class Health(SQLModel): + +class Health(ZooModel): """ Health model """ @@ -19,12 +20,8 @@ class Health(SQLModel): description="The timestamp of the response generated by the server" ) - class Config: - """ - Config for Health - """ - - schema_extra: ClassVar[Dict[str, Any]] = { + model_config = ConfigDict( + json_schema_extra={ "examples": [ { "status": "OK", @@ -33,3 +30,4 @@ class Config: } ] } + )