This commit is contained in:
2024-12-11 16:16:49 +03:00
commit b2699db727
12 changed files with 481 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
__pycache__
*.pyc
*.db
*.log
venv
.venv

32
Dockerfile Normal file
View 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
View 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
View 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"
```

View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,4 @@
fastapi
uvicorn[standard]
sqlalchemy
pydantic

10
schemas.py Normal file
View 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