init
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.db
|
||||
*.log
|
||||
|
||||
venv
|
||||
.venv
|
||||
32
Dockerfile
Normal file
32
Dockerfile
Normal file
@@ -0,0 +1,32 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Устанавливаем переменную окружения для предотвращения создания .pyc файлов
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# Устанавливаем переменную окружения для буферизации вывода
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Копируем файл зависимостей в контейнер
|
||||
COPY requirements.txt .
|
||||
|
||||
# Обновляем pip и устанавливаем зависимости
|
||||
RUN pip install --upgrade pip \
|
||||
&& pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Копируем весь код проекта в контейнер
|
||||
COPY . .
|
||||
|
||||
# Создаем директорию для базы данных и устанавливаем права доступа
|
||||
RUN mkdir -p /app/data \
|
||||
&& chmod -R 755 /app/data
|
||||
|
||||
# Устанавливаем переменную окружения для указания пути к базе данных
|
||||
ENV DATABASE_URL=sqlite:///./data/counters.db
|
||||
|
||||
# Экспонируем порт, на котором будет работать приложение
|
||||
EXPOSE 8000
|
||||
|
||||
# Команда для запуска приложения
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
9
LICENSE
Normal file
9
LICENSE
Normal file
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 depish
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
96
README.md
Normal file
96
README.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Сервис Счетчиков
|
||||
|
||||
**Сервис Счетчиков** на базе FastAPI позволяет инкрементировать, получать и сбрасывать счетчики с поддержкой `namespace`, `application` и `version`. Данные сохраняются в базе данных SQLite.
|
||||
|
||||
## Возможности
|
||||
|
||||
- Инкрементирование счетчика
|
||||
- Получение текущего значения счетчика
|
||||
- Сброс счетчика до нуля
|
||||
- Поддержка пространств имен, приложений и версий
|
||||
- Безопасность при одновременных запросах
|
||||
- Разворачивание через Docker
|
||||
|
||||
## API Документация
|
||||
|
||||
После запуска сервиса, интерактивная документация доступна по адресу: [http://localhost:8000/docs](http://localhost:8000/docs)
|
||||
|
||||
### Эндпоинты
|
||||
|
||||
#### Инкрементировать счетчик
|
||||
|
||||
- **URL**: `POST /{namespace}/{application}/{version}`
|
||||
- **Описание**: Увеличивает счетчик на 1. Если счетчик не существует, создаёт его с начальным значением 1.
|
||||
- **Параметры**:
|
||||
- `namespace` (строка)
|
||||
- `application` (строка)
|
||||
- `version` (строка)
|
||||
- **Ответ**:
|
||||
|
||||
```json
|
||||
{
|
||||
"namespace": "my_namespace",
|
||||
"application": "my_app",
|
||||
"version": "1.0",
|
||||
"value": 1
|
||||
}
|
||||
```
|
||||
|
||||
#### Получить значение счетчика
|
||||
|
||||
- **URL**: `GET /{namespace}/{application}/{version}`
|
||||
- **Описание**: Возвращает текущее значение счетчика. Если счетчик не найден, возвращает 0.
|
||||
- **Параметры**:
|
||||
- `namespace` (строка)
|
||||
- `application` (строка)
|
||||
- `version` (строка)
|
||||
- **Ответ**:
|
||||
|
||||
```json
|
||||
{
|
||||
"namespace": "my_namespace",
|
||||
"application": "my_app",
|
||||
"version": "1.0",
|
||||
"value": 1
|
||||
}
|
||||
```
|
||||
|
||||
#### Сбросить счетчик
|
||||
|
||||
- **URL**: `DELETE /{namespace}/{application}/{version}`
|
||||
- **Описание**: Сбрасывает счетчик до 0. Если счетчик не существует, создаёт его с значением 0.
|
||||
- **Параметры**:
|
||||
- `namespace` (строка)
|
||||
- `application` (строка)
|
||||
- `version` (строка)
|
||||
- **Ответ**:
|
||||
|
||||
```json
|
||||
{
|
||||
"namespace": "my_namespace",
|
||||
"application": "my_app",
|
||||
"version": "1.0",
|
||||
"value": 0
|
||||
}
|
||||
```
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### Инкрементировать счетчик
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/my_namespace/my_app/1.0" -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
Получить значение счетчика
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:8000/my_namespace/my_app/1.0" -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
Сбросить счетчик
|
||||
|
||||
```bash
|
||||
curl -X DELETE "http://localhost:8000/my_namespace/my_app/1.0" -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
106
client/BuildIncrementerClient.py
Normal file
106
client/BuildIncrementerClient.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import requests
|
||||
from typing import Optional
|
||||
|
||||
class BuildIncrementerClient:
|
||||
"""
|
||||
Клиент для взаимодействия с сервисом счетчиков на базе FastAPI.
|
||||
|
||||
Позволяет инкрементировать, получать и сбрасывать счетчики с поддержкой namespace,
|
||||
application и version.
|
||||
|
||||
Пример использования:
|
||||
client = BuildIncrementerClient(base_url="http://localhost:8000")
|
||||
new_value = client.increment("my_namespace", "my_app", "1.0")
|
||||
current_value = client.get_value("my_namespace", "my_app", "1.0")
|
||||
reset_value = client.reset("my_namespace", "my_app", "1.0")
|
||||
"""
|
||||
|
||||
def __init__(self, base_url: str):
|
||||
"""
|
||||
Инициализирует клиент с базовым URL сервиса.
|
||||
|
||||
Args:
|
||||
base_url (str): Базовый URL сервиса, например, "http://localhost:8000"
|
||||
"""
|
||||
self.base_url = base_url.rstrip('/')
|
||||
|
||||
def increment(self, namespace: str, application: str, version: str) -> int:
|
||||
"""
|
||||
Инкрементирует счетчик и возвращает новое значение.
|
||||
|
||||
Args:
|
||||
namespace (str): Пространство имен.
|
||||
application (str): Имя приложения.
|
||||
version (str): Версия приложения.
|
||||
|
||||
Returns:
|
||||
int: Новое значение счетчика.
|
||||
|
||||
Raises:
|
||||
requests.HTTPError: Если запрос завершился с ошибкой.
|
||||
"""
|
||||
url = f"{self.base_url}/{namespace}/{application}/{version}"
|
||||
response = requests.post(url)
|
||||
self._handle_response(response)
|
||||
return response.json()['value']
|
||||
|
||||
def get_value(self, namespace: str, application: str, version: str) -> int:
|
||||
"""
|
||||
Получает текущее значение счетчика.
|
||||
|
||||
Args:
|
||||
namespace (str): Пространство имен.
|
||||
application (str): Имя приложения.
|
||||
version (str): Версия приложения.
|
||||
|
||||
Returns:
|
||||
int: Текущее значение счетчика.
|
||||
|
||||
Raises:
|
||||
requests.HTTPError: Если запрос завершился с ошибкой.
|
||||
"""
|
||||
url = f"{self.base_url}/{namespace}/{application}/{version}"
|
||||
response = requests.get(url)
|
||||
self._handle_response(response)
|
||||
return response.json()['value']
|
||||
|
||||
def reset(self, namespace: str, application: str, version: str) -> int:
|
||||
"""
|
||||
Сбрасывает счетчик до 0 и возвращает новое значение.
|
||||
|
||||
Args:
|
||||
namespace (str): Пространство имен.
|
||||
application (str): Имя приложения.
|
||||
version (str): Версия приложения.
|
||||
|
||||
Returns:
|
||||
int: Новое значение счетчика после сброса.
|
||||
|
||||
Raises:
|
||||
requests.HTTPError: Если запрос завершился с ошибкой.
|
||||
"""
|
||||
url = f"{self.base_url}/{namespace}/{application}/{version}"
|
||||
response = requests.delete(url)
|
||||
self._handle_response(response)
|
||||
return response.json()['value']
|
||||
|
||||
def _handle_response(self, response: requests.Response):
|
||||
"""
|
||||
Обрабатывает ответ от сервера, выбрасывая исключение при ошибке.
|
||||
|
||||
Args:
|
||||
response (requests.Response): Ответ от сервера.
|
||||
|
||||
Raises:
|
||||
requests.HTTPError: Если ответ содержит ошибку.
|
||||
"""
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except requests.HTTPError as e:
|
||||
try:
|
||||
error_detail = response.json().get('detail', '')
|
||||
raise requests.HTTPError(f"{e}, Detail: {error_detail}") from None
|
||||
except ValueError:
|
||||
# Если ответ не JSON, просто выбросить исходную ошибку
|
||||
raise
|
||||
|
||||
13
database.py
Normal file
13
database.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
SQLALCHEMY_DATABASE_URL = "sqlite:///./counters.db"
|
||||
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} # Только для SQLite
|
||||
)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
Base = declarative_base()
|
||||
60
linux/create_service.sh
Executable file
60
linux/create_service.sh
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Имя сервиса
|
||||
SERVICE_NAME="BuildIncrementer"
|
||||
APP_DIR="/opt/BuildIncrementer"
|
||||
VENV_PATH="$APP_DIR/.venv"
|
||||
PORT=7898
|
||||
|
||||
# Полный путь к исполняемому файлу uvicorn
|
||||
EXEC_START="$VENV_PATH/bin/uvicorn main:app --host 0.0.0.0 --port $PORT"
|
||||
|
||||
# Файл службы systemd
|
||||
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
|
||||
|
||||
# Проверка, что директория приложения существует
|
||||
if [ ! -d "$APP_DIR" ]; then
|
||||
echo "Директория приложения не найдена: $APP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Проверка, что виртуальное окружение существует
|
||||
if [ ! -d "$VENV_PATH" ]; then
|
||||
echo "Виртуальное окружение не найдено: $VENV_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Создание файла службы systemd
|
||||
echo "Создаю файл службы systemd: $SERVICE_FILE"
|
||||
|
||||
sudo bash -c "cat > $SERVICE_FILE" <<EOL
|
||||
[Unit]
|
||||
Description=Counter Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$(whoami)
|
||||
WorkingDirectory=$APP_DIR
|
||||
ExecStart=$EXEC_START
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOL
|
||||
|
||||
# Перезагрузка конфигурации systemd
|
||||
echo "Перезагружаю конфигурацию systemd..."
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# Включение сервиса для автозапуска при старте системы
|
||||
echo "Включаю сервис $SERVICE_NAME для автозапуска..."
|
||||
sudo systemctl enable $SERVICE_NAME
|
||||
|
||||
# Запуск сервиса
|
||||
echo "Запускаю сервис $SERVICE_NAME..."
|
||||
sudo systemctl start $SERVICE_NAME
|
||||
|
||||
# Проверка статуса сервиса
|
||||
echo "Проверяю статус сервиса $SERVICE_NAME..."
|
||||
sudo systemctl status $SERVICE_NAME --no-pager
|
||||
31
linux/remove_service.sh
Executable file
31
linux/remove_service.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Имя сервиса
|
||||
SERVICE_NAME="BuildIncrementer"
|
||||
|
||||
# Файл службы systemd
|
||||
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
|
||||
|
||||
# Остановка сервиса
|
||||
echo "Останавливаю сервис $SERVICE_NAME..."
|
||||
sudo systemctl stop $SERVICE_NAME
|
||||
|
||||
# Отключение автозапуска сервиса
|
||||
echo "Отключаю автозапуск сервиса $SERVICE_NAME..."
|
||||
sudo systemctl disable $SERVICE_NAME
|
||||
|
||||
# Удаление файла службы
|
||||
if [ -f "$SERVICE_FILE" ]; then
|
||||
echo "Удаляю файл службы: $SERVICE_FILE"
|
||||
sudo rm -f $SERVICE_FILE
|
||||
else
|
||||
echo "Файл службы не найден: $SERVICE_FILE"
|
||||
fi
|
||||
|
||||
# Перезагрузка конфигурации systemd
|
||||
echo "Перезагружаю конфигурацию systemd..."
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# Проверка статуса сервиса
|
||||
echo "Проверяю статус сервиса $SERVICE_NAME..."
|
||||
sudo systemctl status $SERVICE_NAME --no-pager
|
||||
99
main.py
Normal file
99
main.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from fastapi import FastAPI, HTTPException, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from database import engine, SessionLocal, Base
|
||||
import models
|
||||
import schemas
|
||||
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.orm import aliased
|
||||
|
||||
app = FastAPI(
|
||||
title="Сервис Счетчиков",
|
||||
description="Сервис для инкрементирования, получения и сброса счетчиков с поддержкой namespaces, приложений и версий.",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# Создаем таблицы в базе данных
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# Зависимость для получения сессии базы данных
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@app.post("/{namespace}/{application}/{version}", response_model=schemas.CounterResponse, summary="Инкрементировать счетчик и получить новое значение")
|
||||
def increment_counter(namespace: str, application: str, version: str, db: Session = Depends(get_db)):
|
||||
try:
|
||||
# Попытка найти существующий счетчик
|
||||
counter = db.query(models.Counter).filter_by(
|
||||
namespace=namespace,
|
||||
application=application,
|
||||
version=version
|
||||
).with_for_update().first()
|
||||
|
||||
if counter:
|
||||
counter.value += 1
|
||||
else:
|
||||
# Если счетчик не существует, создать новый с value=1
|
||||
counter = models.Counter(
|
||||
namespace=namespace,
|
||||
application=application,
|
||||
version=version,
|
||||
value=1
|
||||
)
|
||||
db.add(counter)
|
||||
|
||||
db.commit()
|
||||
db.refresh(counter)
|
||||
return counter
|
||||
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail="Ошибка при доступе к базе данных.")
|
||||
|
||||
@app.get("/{namespace}/{application}/{version}", response_model=schemas.CounterResponse, summary="Получить текущее значение счетчика")
|
||||
def get_counter(namespace: str, application: str, version: str, db: Session = Depends(get_db)):
|
||||
counter = db.query(models.Counter).filter_by(
|
||||
namespace=namespace,
|
||||
application=application,
|
||||
version=version
|
||||
).first()
|
||||
|
||||
if not counter:
|
||||
# Если счетчик не найден, вернуть значение 0
|
||||
return schemas.CounterResponse(
|
||||
namespace=namespace,
|
||||
application=application,
|
||||
version=version,
|
||||
value=0
|
||||
)
|
||||
|
||||
return counter
|
||||
|
||||
@app.delete("/{namespace}/{application}/{version}", response_model=schemas.CounterResponse, summary="Сбросить счетчик до 0")
|
||||
def reset_counter(namespace: str, application: str, version: str, db: Session = Depends(get_db)):
|
||||
counter = db.query(models.Counter).filter_by(
|
||||
namespace=namespace,
|
||||
application=application,
|
||||
version=version
|
||||
).with_for_update().first()
|
||||
|
||||
if counter:
|
||||
counter.value = 0
|
||||
else:
|
||||
# Если счетчик не существует, создать его с value=0
|
||||
counter = models.Counter(
|
||||
namespace=namespace,
|
||||
application=application,
|
||||
version=version,
|
||||
value=0
|
||||
)
|
||||
db.add(counter)
|
||||
|
||||
db.commit()
|
||||
db.refresh(counter)
|
||||
return counter
|
||||
14
models.py
Normal file
14
models.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from sqlalchemy import Column, Integer, String, UniqueConstraint
|
||||
from database import Base
|
||||
|
||||
class Counter(Base):
|
||||
__tablename__ = "counters"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
namespace = Column(String, index=True, nullable=False)
|
||||
application = Column(String, index=True, nullable=False)
|
||||
version = Column(String, index=True, nullable=False)
|
||||
value = Column(Integer, default=0, nullable=False)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('namespace', 'application', 'version', name='_namespace_application_version_uc'),
|
||||
)
|
||||
4
requirments.txt
Normal file
4
requirments.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
sqlalchemy
|
||||
pydantic
|
||||
10
schemas.py
Normal file
10
schemas.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class CounterResponse(BaseModel):
|
||||
namespace: str
|
||||
application: str
|
||||
version: str
|
||||
value: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
Reference in New Issue
Block a user