-
Notifications
You must be signed in to change notification settings - Fork 99
Simple Backend homework #166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Vladimir-koven
wants to merge
10
commits into
gardiys:main
Choose a base branch
from
Vladimir-koven:task_homework
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
4763d12
added basic logic on main.py
Vladimir-koven edbe8fc
added gitignore and update readme.md
Vladimir-koven 4c4d1b0
transferring from RAM to json
Vladimir-koven e4901fc
fix task_manager
Vladimir-koven b8d4e18
fixed bugs
Vladimir-koven 7f14e49
task №2 added cloud_storage jsonbin.io
Vladimir-koven f60b4db
task 3 added pydantic and Cloudflare AI
Vladimir-koven 6162284
task №4 added HTTPClient
Vladimir-koven fa21744
fix errors at main.py and task_manager
Vladimir-koven ced1c02
fix errors at cloud_storage.py and cloudflare_ai.py
Vladimir-koven File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| *.idea/ | ||
| *.json | ||
| *.env | ||
| *.test_cloudflare_ai.py |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| from cloudflare_ai import BaseHTTPClient | ||
| from typing import List | ||
|
|
||
|
|
||
| class JSONBinStorage(BaseHTTPClient): | ||
| def __init__(self, bin_id: str, api_key: str): | ||
| self.bin_id = bin_id | ||
| self.api_key = api_key | ||
| base_url = f'https://api.jsonbin.io/v3/b/{self.bin_id}' | ||
| headers = { | ||
| 'X-Master-Key': self.api_key, | ||
| 'Content-Type': 'application/json' | ||
| } | ||
| super().__init__(base_url, headers) | ||
| self.validate_credentials() | ||
|
|
||
| def validate_credentials(self): | ||
| if not self.bin_id or not self.api_key: | ||
| raise ValueError("JSON_BIN_ID или JSON_API_KEY не найдены") | ||
|
|
||
| def get_service_name(self) -> str: | ||
| return "JSONBin Storage" | ||
|
|
||
| def load_data(self) -> List[dict]: | ||
| result = self._make_request("GET") | ||
| return result.get('record', []) | ||
|
|
||
| def save_data(self, data: List[dict]) -> bool: | ||
| self._make_request("PUT", json=data) | ||
| return True | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import requests | ||
| import os | ||
| from dotenv import load_dotenv | ||
| from pydantic import BaseModel | ||
| from abc import ABC, abstractmethod | ||
| from typing import Dict, Any | ||
|
|
||
| load_dotenv() | ||
|
|
||
| class AIException(Exception): # Для ошибок AI сервиса | ||
| pass | ||
|
|
||
| class BaseHTTPClient(ABC): | ||
| def __init__(self, base_url: str, headers: Dict[str, str], timeout: int = 30): | ||
| self.base_url = base_url | ||
| self.headers = headers | ||
| self.timeout = timeout | ||
|
|
||
| def _make_request(self, method: str, endpoint: str = "", **kwargs) -> Dict[str, Any]: | ||
| """Общий метод для выполнения HTTP запросов""" | ||
| url = f"{self.base_url}{endpoint}" | ||
| try: | ||
| response = requests.request( | ||
| method=method, | ||
| url=url, | ||
| headers=self.headers, | ||
| timeout=self.timeout, | ||
| **kwargs | ||
| ) | ||
| response.raise_for_status() | ||
| return response.json() | ||
| except requests.exceptions.HTTPError as e: | ||
| status_code = e.response.status_code if e.response else "Unknown" | ||
| text = e.response.text if e.response else str(e) | ||
| raise AIException(f"HTTP Error {status_code}: {text}") | ||
| except requests.exceptions.ConnectionError as e: | ||
| raise AIException(f"Connection error: {e}") | ||
| except requests.exceptions.Timeout as e: | ||
| raise AIException(f"Request timeout: {e}") | ||
| except requests.RequestException as e: | ||
| raise AIException(f"Network error: {e}") | ||
|
|
||
| @abstractmethod | ||
| def validate_credentials(self): | ||
| pass | ||
|
|
||
| @abstractmethod | ||
| def get_service_name(self) -> str: | ||
| pass | ||
|
|
||
| class AIRequest(BaseModel): | ||
| messages: list | ||
|
|
||
| class AIResponse(BaseModel): | ||
| response: str | ||
|
|
||
| class CloudflareAI(BaseHTTPClient): | ||
| def __init__(self): | ||
| self.api_token = os.getenv("CF_API_TOKEN") | ||
| self.account_id = os.getenv("CF_ACCOUNT_ID") | ||
| self.validate_credentials() | ||
| base_url = f"https://api.cloudflare.com/client/v4/accounts/{self.account_id}/ai/run/" | ||
| headers = { | ||
| "Authorization": f"Bearer {self.api_token}", | ||
| "Content-Type": "application/json" | ||
| } | ||
| super().__init__(base_url, headers) | ||
| self.validate_credentials() | ||
|
|
||
| def validate_credentials(self): | ||
| if not self.api_token or not self.account_id: | ||
| raise ValueError("CF_API_TOKEN или CF_ACCOUNT_ID не найдены в .env") | ||
|
|
||
| def get_service_name(self) -> str: | ||
| return "Cloudflare AI" | ||
|
|
||
| def generate_solution(self, task_text: str) -> str: | ||
| try: | ||
| payload = AIRequest( | ||
| messages=[ | ||
| {"role": "system", | ||
| "content": "Ты — опытный проектный менеджер. Предложи 3–5 конкретных шагов для решения задачи. Формат: нумерованный список." | ||
| }, | ||
| {"role": "user", | ||
| "content": task_text | ||
| } | ||
| ] | ||
| ) | ||
| result = self._make_request("POST", "@cf/meta/llama-3-8b-instruct", json=payload.model_dump()) | ||
| ai_response = AIResponse(**result["result"]) | ||
| return ai_response.response | ||
| except Exception as e: | ||
| raise AIException(f"AI service error: {e}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,37 @@ | ||
| from fastapi import FastAPI | ||
| from fastapi import FastAPI, HTTPException | ||
| from task_manager import TaskManager, TaskModel, TaskUpdateModel, TaskResponse | ||
| from typing import List | ||
|
|
||
| app = FastAPI() | ||
| task_manager = TaskManager() | ||
|
|
||
| @app.get("/tasks") | ||
| @app.get('/tasks', response_model=List[TaskResponse]) | ||
| def get_tasks(): | ||
| pass | ||
| return task_manager.get_all() | ||
|
|
||
| @app.post("/tasks") | ||
| def create_task(task): | ||
| pass | ||
| @app.post('/tasks', response_model=TaskResponse) | ||
| def create_task(task: TaskModel): | ||
| try: | ||
| task_data = task.model_dump() | ||
| return task_manager.create( | ||
| title=task_data.get('title', ''), | ||
| description=task_data.get('description', ''), | ||
| status=task_data.get('status', False) | ||
| ) | ||
| except Exception as e: | ||
| raise HTTPException(status_code=500, detail=str(e)) | ||
|
|
||
| @app.put("/tasks/{task_id}") | ||
| def update_task(task_id: int): | ||
| pass | ||
| @app.put("/tasks/{task_id}", response_model=TaskResponse) | ||
| def update_task(task_id: str, task_update: TaskUpdateModel): | ||
| update_data = task_update.model_dump(exclude_unset=True) | ||
| update = task_manager.update(task_id, **update_data) | ||
| if not update: | ||
| raise HTTPException(status_code=404, detail='Task not found') | ||
| return update | ||
|
|
||
| @app.delete("/tasks/{task_id}") | ||
| def delete_task(task_id: int): | ||
| pass | ||
| @app.delete('/tasks/{task_id}') | ||
| def delete_task(task_id: str): | ||
| result = task_manager.delete(task_id) | ||
| if not result: | ||
| raise HTTPException(status_code=404, detail='Task not found') | ||
| return {'message': 'Task deleted'} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| Задание №2 | ||
| 1. Проблема хранения данных в оперативной памяти: | ||
| - данные теряются при перезапуске приложения; | ||
| - невозможно масштабировать приложение; | ||
| - утечки памяти и ограничение объемом оперативной памяти. | ||
|
|
||
| 2. Улучшения после того, как список из оперативной памяти изменился на файл проекта: | ||
| - задачи сохраняются между перезапусками приложения; | ||
| - данные хранятся на диске; | ||
| - можно сделать копию JSON файла; | ||
| - данные можно просмотреть и редактировать вручную. | ||
| Избавились ли мы таким способом от хранения состояния или нет? | ||
| - нет, состояние просто перенесено из оперативной памяти на диск. | ||
| Где еще можно хранить задачи и какие есть преимущества и недостатки этих подходов? | ||
| - реляционные базы данных; | ||
| - NoSQL базы данных; | ||
| - In-memory базы данных. | ||
| Состояние гонки - это когда несколько потоков или процессов обращаются к одним и тем же данным, и происходит изменение одним из них. | ||
| Проблемы в бекенде на данном этапе: | ||
| - несколько запросов на обновление одной задачи могут перезаписать друг друга; | ||
| - невозможно проверить, не изменилась ли задача с момента её загрузки клиентом; | ||
| - если один метод упадёт после изменения данных, система останется в несогласованном состоянии; | ||
| - клиент не получает информации, если его изменения были перезаписаны другим запросом. | ||
| Решения: | ||
| - реализовать очередь задач (асинхронный подход), то есть все изменения ставить в очередь, обрабатывать последовательно; | ||
| - источники событий, вместо хранения текущего состояния сохраняются все изменения (события). Текущее состояние вычисляется как результат применения цепочки событий; | ||
| - эмуляция транзакций через многошаговые HTTP‑запросы; | ||
| - сериализация операций через единую очередь (например, RabbitMQ), то есть обрабатываются строго последовательно. |
Binary file not shown.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import os | ||
| import uuid | ||
| import logging | ||
| from cloud_storage import JSONBinStorage | ||
| from cloudflare_ai import CloudflareAI | ||
| from dotenv import load_dotenv | ||
| from pydantic import BaseModel | ||
| from typing import Optional, List | ||
|
|
||
| load_dotenv() | ||
| logger = logging.getLogger(__name__) | ||
|
|
||
| class TaskModel(BaseModel): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. дублирование кода с main.py |
||
| title: str | ||
| description: Optional[str] = None | ||
| status: bool = False | ||
|
|
||
| class TaskUpdateModel(BaseModel): | ||
| title: Optional[str] = None | ||
| description: Optional[str] = None | ||
| status: Optional[bool] = None | ||
|
|
||
| class TaskResponse(TaskModel): | ||
| id: str | ||
| ai_solution: Optional[str] = None | ||
|
|
||
| class TaskManager: | ||
| def __init__(self): | ||
| self.bin_id = os.getenv('JSON_BIN_ID') | ||
| self.api_key = os.getenv('JSON_API_KEY') | ||
| if not self.bin_id or not self.api_key: | ||
| raise ValueError('Не найдены JSON_BIN_ID или JSON_API_KEY') | ||
| self.storage = JSONBinStorage(self.bin_id, self.api_key) | ||
| self.ai = CloudflareAI() | ||
|
|
||
| def _tasks(self) -> List[dict]: | ||
| return self.storage.load_data() | ||
|
|
||
| def get_all(self) -> List[TaskResponse]: | ||
| tasks = self._tasks() | ||
| return [TaskResponse(**task) for task in tasks] | ||
|
|
||
| def _save(self, tasks): | ||
| self.storage.save_data(tasks) | ||
|
|
||
| def create(self, title: str, description: str = '', status: bool = False) -> TaskResponse: | ||
| task_data = TaskModel(title=title, description=description, status=status) | ||
| tasks = self._tasks() | ||
| task_id = str(uuid.uuid4()) | ||
| ai_solution = None | ||
| if task_data.description: | ||
| try: | ||
| ai_solution = self.ai.generate_solution(f'{task_data.title}. {task_data.description}') | ||
| except Exception as e: | ||
| logger.error(f"AI generation failed for task '{task_data.title}': {e}") | ||
|
|
||
| new_task = {'id': task_id, | ||
| 'title': task_data.title, | ||
| 'description': task_data.description, | ||
| 'status': task_data.status, | ||
| 'ai_solution': ai_solution, | ||
| } | ||
| tasks.append(new_task) | ||
| self._save(tasks) | ||
| return TaskResponse(**new_task) | ||
|
|
||
| def update(self, task_id: str, **updates) -> Optional[TaskResponse]: | ||
| update_data = TaskUpdateModel(**updates) | ||
| valid_updates = update_data.model_dump(exclude_unset=True) | ||
| if not valid_updates: | ||
| return None | ||
| tasks = self._tasks() | ||
| for task in tasks: | ||
| if task['id'] == task_id: | ||
| curr_task = TaskResponse(**task) | ||
| updated_task = curr_task.model_copy(update=valid_updates) | ||
| for key, value in valid_updates.items(): | ||
| task[key] = value | ||
| self._save(tasks) | ||
| return updated_task | ||
| return None | ||
|
|
||
| def delete(self, task_id: str) -> bool: | ||
| tasks = self._tasks() | ||
| initial_count = len(tasks) | ||
| tasks = [task for task in tasks if task.get('id') != task_id] | ||
| if len(tasks) == initial_count: | ||
| return False | ||
| self._save(tasks) | ||
| return True | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super должен быть раньше 10 строки