Compare commits
9 Commits
353e68843b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ffe5f8a44 | |||
| 5e54e5b0f6 | |||
| ffbc6bf88b | |||
| 02b8e86ccf | |||
| c9ed1456ce | |||
| 90f82d7f8d | |||
| 36faf3b426 | |||
| 4a71515d0f | |||
| 6ebfc7c7d3 |
@@ -1,2 +1,2 @@
|
|||||||
SECRET_KEY = ""
|
SECRET_KEY=
|
||||||
DATABASE_URL = "postgresql://postgres:postgres@db:5432/foo"
|
DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/foo
|
||||||
|
|||||||
1
Pipfile
1
Pipfile
@@ -17,6 +17,7 @@ passlib = {extras = ["bcrypt"], version = "*"}
|
|||||||
python-dotenv = "*"
|
python-dotenv = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
|
debugpy = "*"
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.10"
|
python_version = "3.10"
|
||||||
|
|||||||
127
Pipfile.lock
generated
127
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "51ce1d8d4bf93d4690769e6f5686aeacbc9497655554e9fbc5fa22b1cf7d376d"
|
"sha256": "fb1201a79d19ca6b88a4ddab6556fdc9328a4303be70f7ee913d73fbb69345f9"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -221,7 +221,7 @@
|
|||||||
"sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e",
|
"sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e",
|
||||||
"sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"
|
"sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6' and python_full_version < '4.0.0'",
|
"markers": "python_version >= '3.6' and python_version < '4'",
|
||||||
"version": "==2.2.1"
|
"version": "==2.2.1"
|
||||||
},
|
},
|
||||||
"ecdsa": {
|
"ecdsa": {
|
||||||
@@ -682,7 +682,7 @@
|
|||||||
"sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7",
|
"sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7",
|
||||||
"sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"
|
"sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6' and python_full_version < '4.0.0'",
|
"markers": "python_version >= '3.6' and python_version < '4'",
|
||||||
"version": "==4.9"
|
"version": "==4.9"
|
||||||
},
|
},
|
||||||
"six": {
|
"six": {
|
||||||
@@ -702,54 +702,52 @@
|
|||||||
"version": "==1.3.0"
|
"version": "==1.3.0"
|
||||||
},
|
},
|
||||||
"sqlalchemy": {
|
"sqlalchemy": {
|
||||||
"extras": [
|
"extras": [],
|
||||||
"asyncio"
|
|
||||||
],
|
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:01aa76f324c9bbc0dcb2bc3d9e2a9d7ede4808afa1c38d40d5e2007e3163b206",
|
"sha256:0002e829142b2af00b4eaa26c51728f3ea68235f232a2e72a9508a3116bd6ed0",
|
||||||
"sha256:06055476d38ed7915eeed22b78580556d446d175c3574a01b9eb04d91f3a8b2e",
|
"sha256:0005bd73026cd239fc1e8ccdf54db58b6193be9a02b3f0c5983808f84862c767",
|
||||||
"sha256:081e2a2d75466353c738ca2ee71c0cfb08229b4f9909b5fa085f75c48d021471",
|
"sha256:0292f70d1797e3c54e862e6f30ae474014648bc9c723e14a2fda730adb0a9791",
|
||||||
"sha256:099efef0de9fbda4c2d7cb129e4e7f812007901942259d4e6c6e19bd69de1088",
|
"sha256:036d8472356e1d5f096c5e0e1a7e0f9182140ada3602f8fff6b7329e9e7cfbcd",
|
||||||
"sha256:0e068b8414d60dd35d43c693555fc3d2e1d822cef07960bb8ca3f1ee6c4ff762",
|
"sha256:05f0de3a1dc3810a776275763764bb0015a02ae0f698a794646ebc5fb06fad33",
|
||||||
"sha256:13578d1cda69bc5e76c59fec9180d6db7ceb71c1360a4d7861c37d87ea6ca0b1",
|
"sha256:0990932f7cca97fece8017414f57fdd80db506a045869d7ddf2dda1d7cf69ecc",
|
||||||
"sha256:16ad798fc121cad5ea019eb2297127b08c54e1aa95fe17b3fea9fdbc5c34fe62",
|
"sha256:13e397a9371ecd25573a7b90bd037db604331cf403f5318038c46ee44908c44d",
|
||||||
"sha256:1a92685db3b0682776a5abcb5f9e9addb3d7d9a6d841a452a17ec2d8d457bea7",
|
"sha256:14576238a5f89bcf504c5f0a388d0ca78df61fb42cb2af0efe239dc965d4f5c9",
|
||||||
"sha256:26b8424b32eeefa4faad21decd7bdd4aade58640b39407bf43e7d0a7c1bc0453",
|
"sha256:199a73c31ac8ea59937cc0bf3dfc04392e81afe2ec8a74f26f489d268867846c",
|
||||||
"sha256:29a29d02c9e6f6b105580c5ed7afb722b97bc2e2fdb85e1d45d7ddd8440cfbca",
|
"sha256:2082a2d2fca363a3ce21cfa3d068c5a1ce4bf720cf6497fb3a9fc643a8ee4ddd",
|
||||||
"sha256:2d1539fbc82d2206380a86d6d7d0453764fdca5d042d78161bbfb8dd047c80ec",
|
"sha256:22ff16cedab5b16a0db79f1bc99e46a6ddececb60c396562e50aab58ddb2871c",
|
||||||
"sha256:2d6f178ff2923730da271c8aa317f70cf0df11a4d1812f1d7a704b1cf29c5fe3",
|
"sha256:2307495d9e0ea00d0c726be97a5b96615035854972cc538f6e7eaed23a35886c",
|
||||||
"sha256:2db887dbf05bcc3151de1c4b506b14764c6240a42e844b4269132a7584de1e5f",
|
"sha256:2ad2b727fc41c7f8757098903f85fafb4bf587ca6605f82d9bf5604bd9c7cded",
|
||||||
"sha256:416fe7d228937bd37990b5a429fd00ad0e49eabcea3455af7beed7955f192edd",
|
"sha256:2d6495f84c4fd11584f34e62f9feec81bf373787b3942270487074e35cbe5330",
|
||||||
"sha256:445914dcadc0b623bd9851260ee54915ecf4e3041a62d57709b18a0eed19f33b",
|
"sha256:361f6b5e3f659e3c56ea3518cf85fbdae1b9e788ade0219a67eeaaea8a4e4d2a",
|
||||||
"sha256:52b90c9487e4449ad954624d01dea34c90cd8c104bce46b322c83654f37a23c5",
|
"sha256:3e2ef592ac3693c65210f8b53d0edcf9f4405925adcfc031ff495e8d18169682",
|
||||||
"sha256:55ddb5585129c5d964a537c9e32a8a68a8c6293b747f3fa164e1c034e1657a98",
|
"sha256:4676d51c9f6f6226ae8f26dc83ec291c088fe7633269757d333978df78d931ab",
|
||||||
"sha256:561605cfc26273825ed2fb8484428faf36e853c13e4c90c61c58988aeccb34ed",
|
"sha256:4ba7e122510bbc07258dc42be6ed45997efdf38129bde3e3f12649be70683546",
|
||||||
"sha256:5953e225be47d80410ae519f865b5c341f541d8e383fb6d11f67fb71a45bf890",
|
"sha256:5102fb9ee2c258a2218281adcb3e1918b793c51d6c2b4666ce38c35101bb940e",
|
||||||
"sha256:6a91b7883cb7855a27bc0637166eed622fdf1bb94a4d1630165e5dd88c7e64d3",
|
"sha256:5323252be2bd261e0aa3f33cb3a64c45d76829989fa3ce90652838397d84197d",
|
||||||
"sha256:6cd53b4c756a6f9c6518a3dc9c05a38840f9ae442c91fe1abde50d73651b6922",
|
"sha256:58bb65b3274b0c8a02cea9f91d6f44d0da79abc993b33bdedbfec98c8440175a",
|
||||||
"sha256:715f5859daa3bee6ecbad64501637fa4640ca6734e8cda6135e3898d5f8ccadd",
|
"sha256:59bdc291165b6119fc6cdbc287c36f7f2859e6051dd923bdf47b4c55fd2f8bd0",
|
||||||
"sha256:7e32ce2584564d9e068bb7e0ccd1810cbb0a824c0687f8016fe67e97c345a637",
|
"sha256:5facb7fd6fa8a7353bbe88b95695e555338fb038ad19ceb29c82d94f62775a05",
|
||||||
"sha256:88f4ad3b081c0dbb738886f8d425a5d983328670ee83b38192687d78fc82bd1e",
|
"sha256:639e1ae8d48b3c86ffe59c0daa9a02e2bfe17ca3d2b41611b30a0073937d4497",
|
||||||
"sha256:96821d806c0c90c68ce3f2ce6dd529c10e5d7587961f31dd5c30e3bfddc4545d",
|
"sha256:8eb8897367a21b578b26f5713833836f886817ee2ffba1177d446fa3f77e67c8",
|
||||||
"sha256:9a21c1fb71c69c8ec65430160cd3eee44bbcea15b5a4e556f29d03f246f425ec",
|
"sha256:90484a2b00baedad361402c257895b13faa3f01780f18f4a104a2f5c413e4536",
|
||||||
"sha256:9b7025d46aba946272f6b6b357a22f3787473ef27451f342df1a2a6de23743e3",
|
"sha256:9c56e19780cd1344fcd362fd6265a15f48aa8d365996a37fab1495cae8fcd97d",
|
||||||
"sha256:a3bcd5e2049ceb97e8c273e6a84ff4abcfa1dc47b6d8bbd36e07cce7176610d3",
|
"sha256:b67fc780cfe2b306180e56daaa411dd3186bf979d50a6a7c2a5b5036575cbdbb",
|
||||||
"sha256:a62ae2ea3b940ce9c9cbd675489c2047921ce0a79f971d3082978be91bd58117",
|
"sha256:c0dcf127bb99458a9d211e6e1f0f3edb96c874dd12f2503d4d8e4f1fd103790b",
|
||||||
"sha256:a87f8595390764db333a1705591d0934973d132af607f4fa8b792b366eacbb3c",
|
"sha256:c23d64a0b28fc78c96289ffbd0d9d1abd48d267269b27f2d34e430ea73ce4b26",
|
||||||
"sha256:c8051bff4ce48cbc98f11e95ac46bfd1e36272401070c010248a3230d099663f",
|
"sha256:ccfd238f766a5bb5ee5545a62dd03f316ac67966a6a658efb63eeff8158a4bbf",
|
||||||
"sha256:ca152ffc7f0aa069c95fba46165030267ec5e4bb0107aba45e5e9e86fe4d9363",
|
"sha256:cd767cf5d7252b1c88fcfb58426a32d7bd14a7e4942497e15b68ff5d822b41ad",
|
||||||
"sha256:cd95a3e6ab46da2c5b0703e797a772f3fab44d085b3919a4f27339aa3b1f51d3",
|
"sha256:ce8feaa52c1640de9541eeaaa8b5fb632d9d66249c947bb0d89dd01f87c7c288",
|
||||||
"sha256:d458fd0566bc9e10b8be857f089e96b5ca1b1ef033226f24512f9ffdf485a8c0",
|
"sha256:d2e054aed4645f9b755db85bc69fc4ed2c9020c19c8027976f66576b906a74f1",
|
||||||
"sha256:db3ccbce4a861bf4338b254f95916fc68dd8b7aa50eea838ecdaf3a52810e9c0",
|
"sha256:e16c2be5cb19e2c08da7bd3a87fed2a0d4e90065ee553a940c4fc1a0fb1ab72b",
|
||||||
"sha256:dc10423b59d6d032d6dff0bb42aa06dc6a8824eb6029d70c7d1b6981a2e7f4d8",
|
"sha256:e4b12e3d88a8fffd0b4ca559f6d4957ed91bd4c0613a4e13846ab8729dc5c251",
|
||||||
"sha256:e91a5e45a2ea083fe344b3503405978dff14d60ef3aa836432c9ca8cd47806b6",
|
"sha256:e570cfc40a29d6ad46c9aeaddbdcee687880940a3a327f2c668dd0e4ef0a441d",
|
||||||
"sha256:f1d3fb02a4d0b07d1351a4a52f159e5e7b3045c903468b7e9349ebf0020ffdb9",
|
"sha256:eb30cf008850c0a26b72bd1b9be6730830165ce049d239cfdccd906f2685f892",
|
||||||
"sha256:f61e54b8c2b389de1a8ad52394729c478c67712dbdcdadb52c2575e41dae94a5",
|
"sha256:f37fa70d95658763254941ddd30ecb23fc4ec0c5a788a7c21034fc2305dab7cc",
|
||||||
"sha256:f7944b04e6fcf8d733964dd9ee36b6a587251a1a4049af3a9b846f6e64eb349a",
|
"sha256:f5ebeeec5c14533221eb30bad716bc1fd32f509196318fb9caa7002c4a364e4c",
|
||||||
"sha256:fd69850860093a3f69fefe0ab56d041edfdfe18510b53d9a2eaecba2f15fa795"
|
"sha256:f5fa526d027d804b1f85cdda1eb091f70bde6fb7d87892f6dd5a48925bc88898"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.4.45"
|
"version": "==1.4.41"
|
||||||
},
|
},
|
||||||
"sqlalchemy2-stubs": {
|
"sqlalchemy2-stubs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -761,11 +759,11 @@
|
|||||||
},
|
},
|
||||||
"sqlmodel": {
|
"sqlmodel": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3b4f966b9671b24d85529d274e6c4dbc7753b468e35d2d6a40bd75cad1f66813",
|
"sha256:0fd805719e0c5d4f22be32eb3ffc856eca3f7f20e8c7aa3e117ad91684b518ee",
|
||||||
"sha256:c5fd8719e09da348cd32ce2a5b6a44f289d3029fa8f1c9818229b6f34f1201b4"
|
"sha256:3371b4d1ad59d2ffd0c530582c2140b6c06b090b32af9b9c6412986d7b117036"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.0.6"
|
"version": "==0.0.8"
|
||||||
},
|
},
|
||||||
"starlette": {
|
"starlette": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -996,5 +994,30 @@
|
|||||||
"version": "==10.4"
|
"version": "==10.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"develop": {}
|
"develop": {
|
||||||
|
"debugpy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:143f79d0798a9acea21cd1d111badb789f19d414aec95fa6389cfea9485ddfb1",
|
||||||
|
"sha256:1caee68f7e254267df908576c0d0938f8f88af16383f172cb9f0602e24c30c01",
|
||||||
|
"sha256:2a39e7da178e1f22f4bc04b57f085e785ed1bcf424aaf318835a1a7129eefe35",
|
||||||
|
"sha256:3d9c31baf64bf959a593996c108e911c5a9aa1693a296840e5469473f064bcec",
|
||||||
|
"sha256:40e2a83d31a16b83666f19fa06d97b2cc311af88e6266590579737949971a17e",
|
||||||
|
"sha256:4ab5e938925e5d973f567d6ef32751b17d10f3be3a8c4d73c52f53e727f69bf1",
|
||||||
|
"sha256:563f148f94434365ec0ce94739c749aabf60bf67339e68a9446499f3582d62f3",
|
||||||
|
"sha256:62ba4179b372a62abf9c89b56997d70a4100c6dea6c2a4e0e4be5f45920b3253",
|
||||||
|
"sha256:67edf033f9e512958f7b472975ff9d9b7ff64bf4440f6f6ae44afdc66b89e6b6",
|
||||||
|
"sha256:6ae238943482c78867ac707c09122688efb700372b617ffd364261e5e41f7a2f",
|
||||||
|
"sha256:82229790442856962aec4767b98ba2559fe0998f897e9f21fb10b4fd24b6c436",
|
||||||
|
"sha256:86bd25f38f8b6c5d430a5e2931eebbd5f580c640f4819fcd236d0498790c7204",
|
||||||
|
"sha256:d2968e589bda4e485a9c61f113754a28e48d88c5152ed8e0b2564a1fadbe50a5",
|
||||||
|
"sha256:d5ab9bd3f4e7faf3765fd52c7c43c074104ab1e109621dc73219099ed1a5399d",
|
||||||
|
"sha256:d8df268e9f72fc06efc2e75e8dc8e2b881d6a397356faec26efb2ee70b6863b7",
|
||||||
|
"sha256:e62b8034ede98932b92268669318848a0d42133d857087a3b9cec03bb844c615",
|
||||||
|
"sha256:e886a1296cd20a10172e94788009ce74b759e54229ebd64a43fa5c2b4e62cd76",
|
||||||
|
"sha256:ea4bf208054e6d41749f17612066da861dff10102729d32c85b47f155223cf2b"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.6.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ Compact template for FastAPI-based projects. SQLAlchemy ORM and Alembic migratio
|
|||||||
|
|
||||||
## Alembic
|
## Alembic
|
||||||
|
|
||||||
To add new models to migrations you need to import them inside `migrations/env.py`.
|
To add new models to migrations you need to import them inside `migrations/env.py`. Just add `from <appname>.models import *` at the head of file and it should be enough
|
||||||
|
|
||||||
|
And then create migration:
|
||||||
|
|
||||||
|
pipenv run alembic revision --autogenerate -m "<bla-bla>"
|
||||||
|
|
||||||
To quickly migrate:
|
To quickly migrate:
|
||||||
|
|
||||||
docker-compose exec web bash
|
docker-compose exec web bash
|
||||||
pipenv run alembic upgrate head
|
pipenv run alembic upgrade head
|
||||||
|
|
||||||
So that all `.env` varibles will be catched properly
|
So that all `.env` varibles will be catched properly
|
||||||
|
|||||||
21
app/database/db.py
Normal file
21
app/database/db.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||||
|
from app.settings import settings
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
from sqlalchemy.pool import QueuePool
|
||||||
|
|
||||||
|
# Create async engine with connection pooling
|
||||||
|
engine = create_async_engine(
|
||||||
|
settings.DATABASE_URL,
|
||||||
|
echo=True,
|
||||||
|
poolclass=QueuePool,
|
||||||
|
pool_size=5,
|
||||||
|
max_overflow=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create async session factory
|
||||||
|
async_session = sessionmaker(
|
||||||
|
engine, class_=AsyncSession, expire_on_commit=False
|
||||||
|
)
|
||||||
80
app/database/models.py
Normal file
80
app/database/models.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
from typing import List
|
||||||
|
from sqlalchemy import Column, String
|
||||||
|
from sqlalchemy import update as sqlalchemy_update, delete as sqlalchemy_delete
|
||||||
|
from sqlalchemy.future import select
|
||||||
|
|
||||||
|
from app.database.db import Base, async_session
|
||||||
|
|
||||||
|
from uuid import uuid4
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
class BaseCRUD(Base):
|
||||||
|
__abstract__ = True
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"<{self.__class__.__name__}("
|
||||||
|
f"id={self.id}, "
|
||||||
|
f"name={self.name}, "
|
||||||
|
f")>"
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create(cls, **kwargs) -> Self:
|
||||||
|
async with async_session() as db:
|
||||||
|
server = cls(id=str(uuid4()), **kwargs)
|
||||||
|
db.add(server)
|
||||||
|
try:
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(server)
|
||||||
|
except Exception:
|
||||||
|
await db.rollback()
|
||||||
|
raise
|
||||||
|
return server
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def update(cls, id, **kwargs) -> Self:
|
||||||
|
async with async_session() as db:
|
||||||
|
query = (
|
||||||
|
sqlalchemy_update(cls)
|
||||||
|
.where(cls.id == id)
|
||||||
|
.values(**kwargs)
|
||||||
|
.execution_options(synchronize_session="fetch")
|
||||||
|
)
|
||||||
|
await db.execute(query)
|
||||||
|
try:
|
||||||
|
await db.commit()
|
||||||
|
except Exception:
|
||||||
|
await db.rollback()
|
||||||
|
raise
|
||||||
|
return await cls.get(id)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get(cls, id) -> Self:
|
||||||
|
async with async_session() as db:
|
||||||
|
query = select(cls).where(cls.id == id)
|
||||||
|
servers = await db.execute(query)
|
||||||
|
(server,) = servers.first()
|
||||||
|
return server
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_all(cls) -> List[Self]:
|
||||||
|
async with async_session() as db:
|
||||||
|
query = select(cls)
|
||||||
|
servers = await db.execute(query)
|
||||||
|
servers = servers.scalars().all()
|
||||||
|
return servers
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def delete(cls, id) -> bool:
|
||||||
|
async with async_session() as db:
|
||||||
|
query = sqlalchemy_delete(cls).where(cls.id == id)
|
||||||
|
await db.execute(query)
|
||||||
|
try:
|
||||||
|
await db.commit()
|
||||||
|
except Exception:
|
||||||
|
await db.rollback()
|
||||||
|
raise
|
||||||
|
return True
|
||||||
|
|
||||||
49
app/db.py
49
app/db.py
@@ -1,49 +0,0 @@
|
|||||||
from sqlalchemy import future
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
|
||||||
from sqlalchemy.orm import sessionmaker, declarative_base
|
|
||||||
from app.settings import settings
|
|
||||||
|
|
||||||
Base = declarative_base()
|
|
||||||
|
|
||||||
# engine = create_async_engine(settings.DATABASE_URL, echo=True, future=True)
|
|
||||||
|
|
||||||
# Session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
|
||||||
|
|
||||||
# def get_session():
|
|
||||||
# with Session() as session:
|
|
||||||
# yield session
|
|
||||||
|
|
||||||
class AsyncDatabaseSession:
|
|
||||||
def __init__(self):
|
|
||||||
self._session = None
|
|
||||||
self._engine = None
|
|
||||||
self._engine = create_async_engine(
|
|
||||||
settings.DATABASE_URL,
|
|
||||||
future=True,
|
|
||||||
echo=True,
|
|
||||||
)
|
|
||||||
self._session = sessionmaker(
|
|
||||||
self._engine, expire_on_commit=False, class_=AsyncSession
|
|
||||||
)()
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
return getattr(self._session, name)
|
|
||||||
|
|
||||||
# def init(self):
|
|
||||||
|
|
||||||
db = AsyncDatabaseSession()
|
|
||||||
|
|
||||||
# async def get_session() -> AsyncSession:
|
|
||||||
# async_session = sessionmaker(
|
|
||||||
# engine, class_=AsyncSession, expire_on_commit=False
|
|
||||||
# )
|
|
||||||
# async with async_session() as session:
|
|
||||||
# yield session
|
|
||||||
# # from sqlalchemy import create_engine
|
|
||||||
# # from sqlalchemy.ext.declarative import declarative_base
|
|
||||||
# # from sqlalchemy.orm import sessionmaker
|
|
||||||
|
|
||||||
# engine = create_engine(settings.DATABASE_URL)
|
|
||||||
# SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
||||||
|
|
||||||
|
|
||||||
15
app/main.py
15
app/main.py
@@ -4,6 +4,21 @@ from users.views import router as users_router
|
|||||||
|
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
|
# Workaround to debug `422 Unprocessable Entity` error
|
||||||
|
import logging
|
||||||
|
from fastapi import Request, status
|
||||||
|
from fastapi.exceptions import RequestValidationError
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
@app.exception_handler(RequestValidationError)
|
||||||
|
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||||
|
exc_str = f'{exc}'.replace('\n', ' ').replace(' ', ' ')
|
||||||
|
logging.error(f"{request}: {exc_str}")
|
||||||
|
content = {'status_code': 10422, 'message': exc_str, 'data': None}
|
||||||
|
return JSONResponse(content=content, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
|
||||||
|
# workaround end
|
||||||
|
|
||||||
app.include_router(users_router)
|
app.include_router(users_router)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,20 +11,21 @@ services:
|
|||||||
- POSTGRES_DB=foo
|
- POSTGRES_DB=foo
|
||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
||||||
command: bash -c "pipenv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload"
|
command: /code/entrypoint.sh
|
||||||
volumes:
|
volumes:
|
||||||
- .:/code
|
- .:/code
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
|
- "5678:5678"
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
|
||||||
pgadmin:
|
# pgadmin:
|
||||||
image: dpage/pgadmin4
|
# image: dpage/pgadmin4
|
||||||
environment:
|
# environment:
|
||||||
- PGADMIN_DEFAULT_EMAIL=pgadmin4@pgadmin.org
|
# - PGADMIN_DEFAULT_EMAIL=pgadmin4@pgadmin.org
|
||||||
- PGADMIN_DEFAULT_PASSWORD=admin
|
# - PGADMIN_DEFAULT_PASSWORD=admin
|
||||||
ports:
|
# ports:
|
||||||
- "5050:80"
|
# - "5050:80"
|
||||||
depends_on:
|
# depends_on:
|
||||||
- db
|
# - db
|
||||||
|
|||||||
8
entrypoint.sh
Executable file
8
entrypoint.sh
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
export $(grep -v '^#' .env | xargs -d '\n')
|
||||||
|
# tail -f /dev/null
|
||||||
|
# python -m debugpy --wait-for-client --listen 0.0.0.0:5678 -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
|
||||||
@@ -9,7 +9,9 @@ from sqlalchemy.ext.asyncio import AsyncEngine
|
|||||||
from alembic import context
|
from alembic import context
|
||||||
|
|
||||||
from app.settings import settings
|
from app.settings import settings
|
||||||
import users
|
from app.database import db
|
||||||
|
|
||||||
|
# your models to import for migrations processing
|
||||||
from users.models import *
|
from users.models import *
|
||||||
|
|
||||||
# this is the Alembic Config object, which provides
|
# this is the Alembic Config object, which provides
|
||||||
@@ -27,7 +29,7 @@ config.set_main_option('sqlalchemy.url', settings.DATABASE_URL)
|
|||||||
# for 'autogenerate' support
|
# for 'autogenerate' support
|
||||||
# from myapp import mymodel
|
# from myapp import mymodel
|
||||||
# target_metadata = mymodel.Base.metadata
|
# target_metadata = mymodel.Base.metadata
|
||||||
target_metadata = [users.models.Base.metadata]
|
target_metadata = db.Base.metadata
|
||||||
|
|
||||||
# other values from the config, defined by the needs of env.py,
|
# other values from the config, defined by the needs of env.py,
|
||||||
# can be acquired:
|
# can be acquired:
|
||||||
|
|||||||
113
users/models.py
113
users/models.py
@@ -1,16 +1,21 @@
|
|||||||
# from sqlmodel import SQLModel, Field
|
from sqlalchemy import Column, String, DateTime
|
||||||
from sqlalchemy import Column, String, DateTime, delete
|
from sqlalchemy import update as sqlalchemy_update
|
||||||
from sqlalchemy import update as sqlalchemy_update, delete as sqlalchemy_delete
|
|
||||||
from sqlalchemy.future import select
|
from sqlalchemy.future import select
|
||||||
from app.db import Base, db
|
from app.database.models import BaseCRUD
|
||||||
|
from app.database.db import async_session
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
|
||||||
class User(Base):
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
|
class User(BaseCRUD):
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
id = Column(String, primary_key=True)
|
username = Column(String, nullable=False, unique=True)
|
||||||
username = Column(String)
|
full_name = Column(String)
|
||||||
|
email = Column(String, nullable=False, unique=True)
|
||||||
|
password = Column(String, nullable=False)
|
||||||
created_at = Column(DateTime, index=True, default=datetime.utcnow)
|
created_at = Column(DateTime, index=True, default=datetime.utcnow)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@@ -21,56 +26,60 @@ class User(Base):
|
|||||||
f")>"
|
f")>"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def verify_password(self, plain_password):
|
||||||
|
return pwd_context.verify(plain_password, self.password)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_password_hash(password):
|
||||||
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def authenticate_user(cls, username: str, password: str):
|
||||||
|
# TODO: add exception when noone found
|
||||||
|
async with async_session() as db:
|
||||||
|
query = select(cls).where(cls.username == username)
|
||||||
|
users = await db.execute(query)
|
||||||
|
(user,) = users.first()
|
||||||
|
|
||||||
|
if user:
|
||||||
|
if not cls.verify_password(user, password):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create(cls, **kwargs):
|
async def create(cls, **kwargs):
|
||||||
user = cls(id=str(uuid4()), **kwargs)
|
if 'plain_password' in kwargs:
|
||||||
db.add(user)
|
kwargs['password'] = cls.get_password_hash(kwargs.pop('plain_password'))
|
||||||
try:
|
async with async_session() as db:
|
||||||
await db.commit()
|
user = cls(id=str(uuid4()), **kwargs)
|
||||||
except Exception:
|
db.add(user)
|
||||||
await db.rollback()
|
try:
|
||||||
raise
|
await db.commit()
|
||||||
return user
|
await db.refresh()
|
||||||
|
except Exception:
|
||||||
|
await db.rollback()
|
||||||
|
raise
|
||||||
|
return user
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def update(cls, id, **kwargs):
|
async def update(cls, id, **kwargs):
|
||||||
query = (
|
if 'plain_password' in kwargs:
|
||||||
sqlalchemy_update(cls)
|
kwargs['password'] = cls.get_password_hash(kwargs.pop('plain_password'))
|
||||||
.where(cls.id == id)
|
async with async_session() as db:
|
||||||
.values(**kwargs)
|
query = (
|
||||||
.execution_options(synchronize_session="fetch")
|
sqlalchemy_update(cls)
|
||||||
)
|
.where(cls.id == id)
|
||||||
await db.execute(query)
|
.values(**kwargs)
|
||||||
try:
|
.execution_options(synchronize_session="fetch")
|
||||||
await db.commit()
|
)
|
||||||
except Exception:
|
await db.execute(query)
|
||||||
await db.rollback()
|
try:
|
||||||
raise
|
await db.commit()
|
||||||
|
except Exception:
|
||||||
@classmethod
|
await db.rollback()
|
||||||
async def get(cls, id):
|
raise
|
||||||
query = select(cls).where(cls.id == id)
|
return await cls.get(id)
|
||||||
users = await db.execute(query)
|
|
||||||
(user,) = users.first()
|
|
||||||
return user
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def get_all(cls):
|
|
||||||
query = select(cls)
|
|
||||||
users = await db.execute(query)
|
|
||||||
users = users.scalars().all()
|
|
||||||
return users
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def delete(cls, id):
|
|
||||||
query = sqlalchemy_delete(cls).where(cls.id == id)
|
|
||||||
await db.execute(query)
|
|
||||||
try:
|
|
||||||
await db.commit()
|
|
||||||
except Exception:
|
|
||||||
await db.rollback()
|
|
||||||
raise
|
|
||||||
return True
|
|
||||||
|
|
||||||
# class UserBase(SQLModel):
|
# class UserBase(SQLModel):
|
||||||
# username: str
|
# username: str
|
||||||
|
|||||||
164
users/views.py
164
users/views.py
@@ -3,13 +3,8 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
|||||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from sqlalchemy import select
|
|
||||||
|
|
||||||
from passlib.context import CryptContext
|
|
||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
|
|
||||||
# from app.db import get_session
|
|
||||||
from app.settings import settings
|
from app.settings import settings
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import List
|
from typing import List
|
||||||
@@ -19,97 +14,56 @@ from users.models import User
|
|||||||
ALGORITHM = "HS256"
|
ALGORITHM = "HS256"
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/users/token")
|
||||||
|
|
||||||
# router = APIRouter()
|
|
||||||
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
|
||||||
|
to_encode = data.copy()
|
||||||
|
if expires_delta:
|
||||||
|
expire = datetime.utcnow() + expires_delta
|
||||||
|
else:
|
||||||
|
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||||
|
to_encode.update({"exp": expire})
|
||||||
|
|
||||||
# def verify_password(plain_password, hashed_password):
|
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
|
||||||
# return pwd_context.verify(plain_password, hashed_password)
|
|
||||||
|
|
||||||
# def get_password_hash(password):
|
return encoded_jwt
|
||||||
# return pwd_context.hash(password)
|
|
||||||
|
|
||||||
# def authenticate_user(username: str, password: str, db: Session):
|
def verify_access_token(token: str) -> bool:
|
||||||
# statement = select(User).where(User.username == username)
|
try:
|
||||||
# result = db.execute(statement).fetchone()
|
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
|
user_id: str = payload.get("sub", None)
|
||||||
|
if user_id is None:
|
||||||
|
return False
|
||||||
|
except JWTError:
|
||||||
|
return False
|
||||||
|
|
||||||
# if result:
|
return True
|
||||||
# print(result.User.__dict__)
|
|
||||||
# if not verify_password(password, result.User.hashed_password):
|
|
||||||
# return False
|
|
||||||
|
|
||||||
# return result.User
|
|
||||||
|
|
||||||
# def create_access_token(data: dict, expires_delta: timedelta | None = None):
|
|
||||||
# to_encode = data.copy()
|
|
||||||
# if expires_delta:
|
|
||||||
# expire = datetime.utcnow() + expires_delta
|
|
||||||
# else:
|
|
||||||
# expire = datetime.utcnow() + timedelta(minutes=15)
|
|
||||||
# to_encode.update({"exp": expire})
|
|
||||||
# encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
|
|
||||||
|
|
||||||
# return encoded_jwt
|
|
||||||
|
|
||||||
# async def get_current_user(token: str = Depends(oauth2_scheme)):
|
|
||||||
# credentials_exception = HTTPException(
|
|
||||||
# status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
# detail="Could not validate credentials",
|
|
||||||
# headers={"WWW-Authenticate": "Bearer"},
|
|
||||||
# )
|
|
||||||
# try:
|
|
||||||
# payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
|
|
||||||
# username: str = payload.get("sub", None)
|
|
||||||
# if username is None:
|
|
||||||
# raise credentials_exception
|
|
||||||
# token_data = TokenData(username=username)
|
|
||||||
# except JWTError:
|
|
||||||
# raise credentials_exception
|
|
||||||
# user = get_user(username=token_data.username)
|
|
||||||
# if user is None:
|
|
||||||
# raise credentials_exception
|
|
||||||
# return user
|
|
||||||
|
|
||||||
# async def get_current_active_user(current_user: User = Depends(get_current_user)):
|
|
||||||
# if current_user.disabled:
|
|
||||||
# raise HTTPException(status_code=400, detail="Inactive user")
|
|
||||||
# return current_user
|
|
||||||
|
|
||||||
# @router.post("/register/", tags=["users"])
|
|
||||||
# async def register(db: Session = Depends(get_session)):
|
|
||||||
# pass
|
|
||||||
|
|
||||||
# @router.get("/login/", tags=["users"])
|
|
||||||
# async def login(session: Session = Depends(get_session)):
|
|
||||||
# username = "johndoe"
|
|
||||||
# statement = select(User).where(User.username == username)
|
|
||||||
# result = session.execute(statement).first()
|
|
||||||
|
|
||||||
# print(result)
|
|
||||||
|
|
||||||
# return {"hi": "there"}
|
|
||||||
|
|
||||||
# @router.post("/token", tags=["users"])
|
|
||||||
# async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_session)):
|
|
||||||
# user = authenticate_user(form_data.username, form_data.password, db)
|
|
||||||
# if not user:
|
|
||||||
# raise HTTPException(
|
|
||||||
# status_code=HTTP_401_UNAUTHORIZED,
|
|
||||||
# detail="Incorrect username or password",
|
|
||||||
# headers={"WWW-Authenticate": "Bearer"},
|
|
||||||
# )
|
|
||||||
# access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
||||||
# access_token = create_access_token(
|
|
||||||
# data = {"sub": user.username}, expires_delta=access_token_expires
|
|
||||||
# )
|
|
||||||
# return {"access_token": access_token, "token_type": "bearer"}
|
|
||||||
|
|
||||||
|
async def get_current_user(token: str = Depends(oauth2_scheme)):
|
||||||
|
credentials_exception = HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Could not validate credentials",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
|
user_id: str = payload.get("sub", None)
|
||||||
|
if user_id is None:
|
||||||
|
raise credentials_exception
|
||||||
|
except JWTError:
|
||||||
|
raise credentials_exception
|
||||||
|
user = await User.get(user_id)
|
||||||
|
if user is None:
|
||||||
|
raise credentials_exception
|
||||||
|
return user
|
||||||
|
|
||||||
class UserSchema(BaseModel):
|
class UserSchema(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
|
full_name: str
|
||||||
|
plain_password: str
|
||||||
|
email: str
|
||||||
|
|
||||||
class UserSerializer(BaseModel):
|
class UserSerializer(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
@@ -118,8 +72,19 @@ class UserSerializer(BaseModel):
|
|||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
class UserPassVerifySchema(BaseModel):
|
||||||
|
id: str
|
||||||
|
plain_password: str
|
||||||
|
|
||||||
|
class PasswordSerializer(BaseModel):
|
||||||
|
correct_password: bool
|
||||||
|
|
||||||
|
class TokenSchema(BaseModel):
|
||||||
|
token: str
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
prefix="/users",
|
prefix="/users",
|
||||||
|
tags=["users"],
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.post("/", response_model=UserSerializer)
|
@router.post("/", response_model=UserSerializer)
|
||||||
@@ -129,6 +94,15 @@ async def create_user(user: UserSchema):
|
|||||||
user = await User.create(**user.dict())
|
user = await User.create(**user.dict())
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
@router.get("/me", response_model=UserSerializer)
|
||||||
|
async def read_users_me(current_user: User = Depends(get_current_user)):
|
||||||
|
return current_user
|
||||||
|
|
||||||
|
@router.post("/verify", response_model=PasswordSerializer)
|
||||||
|
async def verify_user_password(user: UserPassVerifySchema):
|
||||||
|
us = await User.get(id=user.id)
|
||||||
|
return PasswordSerializer(correct_password=us.verify_password(user.plain_password))
|
||||||
|
|
||||||
@router.get("/{id}", response_model=UserSerializer)
|
@router.get("/{id}", response_model=UserSerializer)
|
||||||
async def get_user(id: str):
|
async def get_user(id: str):
|
||||||
user = await User.get(id)
|
user = await User.get(id)
|
||||||
@@ -147,3 +121,23 @@ async def update(id: str, user: UserSchema):
|
|||||||
@router.delete("/{id}", response_model=bool)
|
@router.delete("/{id}", response_model=bool)
|
||||||
async def delete_user(id: str):
|
async def delete_user(id: str):
|
||||||
return await User.delete(id)
|
return await User.delete(id)
|
||||||
|
|
||||||
|
@router.post("/check-login", response_model=bool)
|
||||||
|
async def check_if_token_still_active(token: TokenSchema):
|
||||||
|
return verify_access_token(token.token)
|
||||||
|
|
||||||
|
@router.post("/token")
|
||||||
|
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||||
|
user = await User.authenticate_user(form_data.username, form_data.password)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Incorrect username or password",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
access_token = create_access_token(
|
||||||
|
data = {"sub": user.id}, expires_delta=access_token_expires
|
||||||
|
)
|
||||||
|
return {"access_token": access_token, "token_type": "bearer"}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user