commit 6e8a1540b49efc3ead5c8205b164ff3609b57757 Author: emmatveev Date: Mon Dec 2 20:10:32 2024 +0300 initial diff --git a/.deploy/deploy-dev.yaml b/.deploy/deploy-dev.yaml new file mode 100644 index 0000000..385b442 --- /dev/null +++ b/.deploy/deploy-dev.yaml @@ -0,0 +1,22 @@ +version: "3.4" + + +services: + locks: + image: mathwave/sprint-repo:locks + networks: + - locks-development + environment: + MONGO_HOST: "mongo.develop.sprinthub.ru" + MONGO_PASSWORD: $MONGO_PASSWORD_DEV + deploy: + mode: replicated + restart_policy: + condition: any + update_config: + parallelism: 1 + order: start-first + +networks: + locks-development: + external: true diff --git a/.deploy/deploy-prod.yaml b/.deploy/deploy-prod.yaml new file mode 100644 index 0000000..0aab243 --- /dev/null +++ b/.deploy/deploy-prod.yaml @@ -0,0 +1,22 @@ +version: "3.4" + + +services: + locks: + image: mathwave/sprint-repo:locks + networks: + - locks + environment: + MONGO_HOST: "mongo.sprinthub.ru" + MONGO_PASSWORD: $MONGO_PASSWORD_PROD + deploy: + mode: replicated + restart_policy: + condition: any + update_config: + parallelism: 1 + order: start-first + +networks: + locks: + external: true diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml new file mode 100644 index 0000000..ccbf438 --- /dev/null +++ b/.gitea/workflows/deploy-dev.yaml @@ -0,0 +1,45 @@ +name: Deploy Dev + +on: + pull_request: + branches: + - dev + types: [closed] + +jobs: + build: + name: Build + runs-on: [ dev ] + steps: + - name: login + run: docker login -u mathwave -p ${{ secrets.DOCKERHUB_PASSWORD }} + - name: checkout + uses: actions/checkout@v4 + with: + ref: dev + - name: build + run: docker build -t mathwave/sprint-repo:locks . + push: + name: Push + runs-on: [ dev ] + needs: build + steps: + - name: push + run: docker push mathwave/sprint-repo:locks + deploy-dev: + name: Deploy dev + runs-on: [prod] + needs: push + steps: + - name: login + run: docker login -u mathwave -p ${{ secrets.DOCKERHUB_PASSWORD }} + - name: checkout + uses: actions/checkout@v4 + with: + ref: dev + - name: network + run: docker network create -d overlay --attachable locks-development || true + - name: deploy + env: + MONGO_PASSWORD_DEV: ${{ secrets.MONGO_PASSWORD_DEV }} + run: docker stack deploy --with-registry-auth -c ./.deploy/deploy-dev.yaml locks-development diff --git a/.gitea/workflows/deploy-prod.yaml b/.gitea/workflows/deploy-prod.yaml new file mode 100644 index 0000000..e2a7d55 --- /dev/null +++ b/.gitea/workflows/deploy-prod.yaml @@ -0,0 +1,45 @@ +name: Deploy Prod + +on: + pull_request: + branches: + - prod + types: [closed] + +jobs: + build: + name: Build + runs-on: [ dev ] + steps: + - name: login + run: docker login -u mathwave -p ${{ secrets.DOCKERHUB_PASSWORD }} + - name: checkout + uses: actions/checkout@v4 + with: + ref: prod + - name: build + run: docker build -t mathwave/sprint-repo:locks . + push: + name: Push + runs-on: [ dev ] + needs: build + steps: + - name: push + run: docker push mathwave/sprint-repo:locks + deploy-prod: + name: Deploy prod + runs-on: [prod] + needs: push + steps: + - name: login + run: docker login -u mathwave -p ${{ secrets.DOCKERHUB_PASSWORD }} + - name: checkout + uses: actions/checkout@v4 + with: + ref: prod + - name: network + run: docker network create -d overlay --attachable locks || true + - name: deploy + env: + MONGO_PASSWORD_PROD: ${{ secrets.MONGO_PASSWORD_PROD }} + run: docker stack deploy --with-registry-auth -c ./.deploy/deploy-prod.yaml locks diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..849d3ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,119 @@ +# Django # +*.log +*.pot +*.pyc +__pycache__ +db.sqlite3 +media +data +*/__pycache__ + +# Backup files # +*.bak + +# If you are using PyCharm # +.idea +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/gradle.xml +.idea/**/libraries +*.iws /out/ + +# Python # +*.py[cod] +*$py.class + +# Distribution / packaging +.Python build/ +develop-eggs/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +.pytest_cache/ +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +postgres-data + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery +celerybeat-schedule.* + +# SageMath parsed files +*.sage.py + +# Environments +.venv +env/ +ENV/ +venv/ +env.bak/ +venv.bak/ + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# Sublime Text # +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache +*.sublime-workspace +*.sublime-project + +# sftp configuration file +sftp-config.json + +# Package control specific files Package +Control.last-run +Control.ca-list +Control.ca-bundle +Control.system-ca-bundle +GitHub.sublime-settings + +# Visual Studio Code # +.vscode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..224e023 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.12 + +RUN mkdir /usr/src/app +WORKDIR /usr/src/app + +COPY requirements.txt requirements.txt +RUN pip install -r requirements.txt + +COPY . . + +ENTRYPOINT ["python", "main.py"] diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routers/__init__.py b/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routers/acquire.py b/app/routers/acquire.py new file mode 100644 index 0000000..e59c843 --- /dev/null +++ b/app/routers/acquire.py @@ -0,0 +1,23 @@ +import datetime +import fastapi +import pydantic + +from app.storage.mongo import locks +from app.utils import time + + +class RequestBody(pydantic.BaseModel): + name: str + ttl: int + + +router = fastapi.APIRouter() + + +@router.post('/api/v1/acquire', status_code=fastapi.status.HTTP_202_ACCEPTED, responses={'409': {'description': 'Conflict'}}) +async def execute(body: RequestBody): + try: + await locks.acquire(locks.Lock(name=body.name, locked_until=time.now() + datetime.timedelta(seconds=body.ttl))) + except Exception as e: + print(e) + raise fastapi.HTTPException(409) diff --git a/app/routers/release.py b/app/routers/release.py new file mode 100644 index 0000000..68947ce --- /dev/null +++ b/app/routers/release.py @@ -0,0 +1,19 @@ +import datetime +import fastapi +import pydantic + +from app.storage.mongo import locks +from app.utils import time + + +class RequestBody(pydantic.BaseModel): + name: str + ttl: int + + +router = fastapi.APIRouter() + + +@router.post('/api/v1/release', status_code=fastapi.status.HTTP_202_ACCEPTED) +async def execute(body: RequestBody): + await locks.release(locks.Lock(name=body.name, locked_until=time.now() + datetime.timedelta(seconds=body.ttl))) diff --git a/app/storage/__init__.py b/app/storage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/storage/mongo/__init__.py b/app/storage/mongo/__init__.py new file mode 100644 index 0000000..31348dc --- /dev/null +++ b/app/storage/mongo/__init__.py @@ -0,0 +1,20 @@ +import os +import motor +import motor.motor_asyncio +import pymongo + + +MONGO_HOST = os.getenv('MONGO_HOST', 'localhost') +MONGO_PASSWORD = os.getenv('MONGO_PASSWORD', 'password') + +CONNECTION_STRING = f'mongodb://mongo:{MONGO_PASSWORD}@{MONGO_HOST}:27017/' + + +database: 'motor.MotorDatabase' = motor.motor_asyncio.AsyncIOMotorClient(CONNECTION_STRING).locks + +def create_indexes(): + client = pymongo.MongoClient(CONNECTION_STRING) + database = client.get_database('locks') + database.get_collection('locks').create_index([ + ('name', 1), + ], unique=True) diff --git a/app/storage/mongo/locks.py b/app/storage/mongo/locks.py new file mode 100644 index 0000000..1e055ad --- /dev/null +++ b/app/storage/mongo/locks.py @@ -0,0 +1,23 @@ +import bson +import datetime +import pydantic + +from app.storage.mongo import database +from app.utils import time +from bson import codec_options + + +collection = database.get_collection("locks", codec_options=codec_options.CodecOptions(tz_aware=True)) + + +class Lock(pydantic.BaseModel): + name: str + locked_until: pydantic.AwareDatetime + + +async def acquire(lock: Lock): + await collection.insert_one(lock.model_dump()) + + +async def release(name: str): + await collection.delete_one({'name': name}) diff --git a/app/utils/time.py b/app/utils/time.py new file mode 100644 index 0000000..b51c065 --- /dev/null +++ b/app/utils/time.py @@ -0,0 +1,4 @@ +import datetime + + +now = lambda: datetime.datetime.now(datetime.UTC) diff --git a/main.py b/main.py new file mode 100644 index 0000000..411a403 --- /dev/null +++ b/main.py @@ -0,0 +1,19 @@ +import fastapi +import uvicorn + +from app.routers import acquire +from app.routers import release + +from app.storage import mongo + + +app = fastapi.FastAPI() + +app.include_router(acquire.router) +app.include_router(release.router) + +mongo.create_indexes() + + +if __name__ == '__main__': + uvicorn.run(app, host="0.0.0.0", port=80) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..325371f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,20 @@ +annotated-types==0.7.0 +anyio==4.6.2.post1 +APScheduler==3.10.4 +click==8.1.7 +dnspython==2.7.0 +fastapi==0.115.4 +h11==0.14.0 +idna==3.10 +motor==3.6.0 +pydantic==2.9.2 +pydantic_core==2.23.4 +pymongo==4.9.2 +pytz==2024.2 +redis==5.2.0 +six==1.16.0 +sniffio==1.3.1 +starlette==0.41.2 +typing_extensions==4.12.2 +tzlocal==5.2 +uvicorn==0.32.0