backup: 2026-04-09 17:52

This commit is contained in:
Dmitry
2026-04-09 17:52:17 +03:00
parent 6573b20105
commit 9677609ef3
39 changed files with 1324 additions and 1403 deletions
-1
View File
@@ -1 +0,0 @@
3.14
-6
View File
@@ -1,6 +0,0 @@
def main():
print("Hello from web!")
if __name__ == "__main__":
main()
@@ -0,0 +1 @@
3.12
@@ -6,7 +6,7 @@ import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mephi_app.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
@@ -1,5 +1,5 @@
"""
ASGI config for web project.
ASGI config for mephi_app project.
It exposes the ASGI callable as a module-level variable named ``application``.
@@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mephi_app.settings')
application = get_asgi_application()
@@ -1,17 +1,6 @@
"""
Django settings for web project.
Generated by 'django-admin startproject' using Django 6.0.3.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.0/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@@ -20,7 +9,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-z3b@_v7nj4&8r426%*m1p8ytqr8@wnik7w5+9knv7+2)b8&eek'
SECRET_KEY = 'django-insecure-#pk6j+5iy8gxi!4o2ll&aci==cbj4e)kxx7f%7u8!#s^o2qb61'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@@ -37,7 +26,7 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'polls',
'polls.apps.PollsConfig',
]
MIDDLEWARE = [
@@ -50,7 +39,7 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'web.urls'
ROOT_URLCONF = 'mephi_app.urls'
TEMPLATES = [
{
@@ -67,7 +56,7 @@ TEMPLATES = [
},
]
WSGI_APPLICATION = 'web.wsgi.application'
WSGI_APPLICATION = 'mephi_app.wsgi.application'
# Database
@@ -103,7 +92,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/6.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = 'ru-ru'
TIME_ZONE = 'UTC'
@@ -1,5 +1,5 @@
"""
URL configuration for web project.
URL configuration for mephi_app project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/6.0/topics/http/urls/
@@ -0,0 +1,7 @@
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mephi_app.settings')
application = get_wsgi_application()
@@ -1,4 +1,4 @@
# Generated by Django 6.0.3 on 2026-03-26 12:57
# Generated by Django 6.0.4 on 2026-04-08 10:39
from django.db import migrations, models
@@ -15,8 +15,8 @@ class Migration(migrations.Migration):
name='Question',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('question_text', models.CharField(max_length=200)),
('pub_date', models.DateTimeField(verbose_name='date published')),
('question_text', models.CharField(max_length=200, verbose_name='Вопрос')),
('pub_date', models.DateTimeField(verbose_name='Дата публикации')),
],
),
]
@@ -0,0 +1,23 @@
# Generated by Django 6.0.4 on 2026-04-08 10:43
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('polls', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Choice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('choice_text', models.CharField(max_length=200, verbose_name='Ответ')),
('votes', models.IntegerField(default=0, verbose_name='Количество голосов')),
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polls.question')),
],
),
]
@@ -0,0 +1,11 @@
from django.db import models
# Create your models here.
class Question(models.Model):
question_text = models.CharField(verbose_name='Вопрос', max_length=200)
pub_date = models.DateTimeField(verbose_name='Дата публикации')
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(verbose_name='Ответ', max_length=200)
votes = models.IntegerField(verbose_name='Количество голосов', default=0)
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
@@ -1,10 +1,9 @@
[project]
name = "web"
name = "web-django"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.14"
requires-python = ">=3.12"
dependencies = [
"django>=6.0.3",
"jupyterlab>=4.5.6",
"django>=6.0.4",
]
+55
View File
@@ -0,0 +1,55 @@
version = 1
revision = 3
requires-python = ">=3.12"
[[package]]
name = "asgiref"
version = "3.11.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" },
]
[[package]]
name = "django"
version = "6.0.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/b9/4155091ad1788b38563bd77a7258c0834e8c12a7f56f6975deaf54f8b61d/django-6.0.4.tar.gz", hash = "sha256:8cfa2572b3f2768b2e84983cf3c4811877a01edb64e817986ec5d60751c113ac", size = 10907407, upload-time = "2026-04-07T13:55:44.961Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/47/3d61d611609764aa71a37f7037b870e7bfb22937366974c4fd46cada7bab/django-6.0.4-py3-none-any.whl", hash = "sha256:14359c809fc16e8f81fd2b59d7d348e4d2d799da6840b10522b6edf7b8afc1da", size = 8368342, upload-time = "2026-04-07T13:55:37.999Z" },
]
[[package]]
name = "sqlparse"
version = "0.5.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
]
[[package]]
name = "tzdata"
version = "2026.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" },
]
[[package]]
name = "web-django"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "django" },
]
[package.metadata]
requires-dist = [{ name = "django", specifier = ">=6.0.4" }]
@@ -1,12 +0,0 @@
from django.db import models
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
def __str__(self):
return self.question_text
# q1 = Question.objects.create(question_text="What's new?")
# print(Question.objects.all())
@@ -1,13 +0,0 @@
from django.test import TestCase
from .models import Question
from django.utils import timezone
class QuestionTest(TestCase):
def test_create_object(self):
obj = Question.objects.create(
question_text="What's new?",
pub_date=timezone.now()
)
self.assertTrue(Question.objects.filter(id=obj.id).exists())
@@ -1,16 +0,0 @@
"""
WSGI config for web project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web.settings')
application = get_wsgi_application()
-1327
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -7,11 +7,13 @@ BASE_URL = "http://127.0.0.1:5000"
def ask_format() -> str:
"""Запрашивает желаемый формат ответа и возвращает json или wsdl."""
raw = input("Формат ответа (json/wsdl) [json]: ").strip().lower()
return raw if raw in {"json", "wsdl"} else "json"
def print_response(resp: requests.Response) -> None:
"""Печатает HTTP-статус и тело ответа в JSON или сыром виде."""
print(f"\nHTTP {resp.status_code}")
try:
print(json.dumps(resp.json(), ensure_ascii=False, indent=2))
@@ -20,6 +22,7 @@ def print_response(resp: requests.Response) -> None:
def input_payload() -> dict:
"""Собирает поля объекта из консоли и возвращает payload для POST/PUT."""
print("Оставьте поле пустым, если не хотите его передавать")
payload = {}
@@ -51,6 +54,7 @@ def input_payload() -> dict:
def list_calls() -> None:
"""Запрашивает и выводит пагинированный список объектов calls."""
page = input("page [1]: ").strip() or "1"
per_page = input("per_page [20]: ").strip() or "20"
fmt = ask_format()
@@ -64,6 +68,7 @@ def list_calls() -> None:
def get_call() -> None:
"""Запрашивает и выводит один объект calls по ID."""
call_id = input("ID объекта: ").strip()
fmt = ask_format()
resp = requests.get(
@@ -73,6 +78,7 @@ def get_call() -> None:
def create_call() -> None:
"""Создает новый объект calls из введенных пользователем данных."""
payload = input_payload()
fmt = ask_format()
resp = requests.post(
@@ -82,6 +88,7 @@ def create_call() -> None:
def update_call() -> None:
"""Обновляет существующий объект calls по ID введенными полями."""
call_id = input("ID объекта: ").strip()
payload = input_payload()
fmt = ask_format()
@@ -95,6 +102,7 @@ def update_call() -> None:
def delete_call() -> None:
"""Удаляет объект calls по ID и выводит результат операции."""
call_id = input("ID объекта: ").strip()
fmt = ask_format()
resp = requests.delete(
@@ -104,6 +112,7 @@ def delete_call() -> None:
def stats_by_hour() -> None:
"""Запрашивает статистику количества обращений по выбранному часу."""
hour = input("Час (0-23): ").strip()
fmt = ask_format()
resp = requests.get(
@@ -113,11 +122,13 @@ def stats_by_hour() -> None:
def show_wsdl() -> None:
"""Получает и выводит WSDL-описание API."""
resp = requests.get(f"{BASE_URL}/api/wsdl", timeout=30)
print_response(resp)
def menu() -> None:
"""Запускает интерактивное меню CLI и обрабатывает выбор пользователя."""
actions = {
"1": ("Список объектов", list_calls),
"2": ("Один объект по ID", get_call),
+58
View File
@@ -13,17 +13,33 @@ app = Flask(__name__)
ALLOWED_FIELDS = ("lat", "lng", "title", "timeStamp", "town", "hour")
def parse_positive_int(raw_value: str, field_name: str) -> tuple[int | None, str | None]:
"""Преобразует строку в положительное целое число или возвращает текст ошибки."""
try:
value = int(raw_value)
except (TypeError, ValueError):
return None, f"{field_name} должен быть целым числом"
if value <= 0:
return None, f"{field_name} должен быть положительным целым числом"
return value, None
def get_db_connection() -> sqlite3.Connection:
"""Создает и возвращает подключение к SQLite с доступом к колонкам по имени."""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def wants_wsdl() -> bool:
"""Проверяет, запросил ли клиент XML/WSDL-формат через query-параметр format."""
return request.args.get("format", "json").lower() == "wsdl"
def dict_to_xml(payload: dict) -> bytes:
"""Преобразует словарь (включая вложенные dict/list) в XML-документ."""
root = Element("response")
for key, value in payload.items():
@@ -53,6 +69,7 @@ def dict_to_xml(payload: dict) -> bytes:
def format_response(payload: dict, status: int = 200) -> Response:
"""Формирует ответ API: XML при format=wsdl, иначе JSON."""
if wants_wsdl():
xml_payload = dict_to_xml(payload)
return Response(xml_payload, status=status, mimetype="application/wsdl+xml")
@@ -62,6 +79,7 @@ def format_response(payload: dict, status: int = 200) -> Response:
def fetch_call_by_id(conn: sqlite3.Connection, call_id: int):
"""Возвращает запись calls по rowid или None, если запись не найдена."""
query = """
SELECT rowid AS id, lat, lng, title, timeStamp, town, hour
FROM calls
@@ -71,6 +89,7 @@ def fetch_call_by_id(conn: sqlite3.Connection, call_id: int):
def parse_payload() -> tuple[dict, str | None]:
"""Читает JSON-тело запроса, валидирует и оставляет только разрешенные поля."""
if not request.is_json:
return {}, "Body должен быть в формате JSON"
@@ -87,6 +106,7 @@ def parse_payload() -> tuple[dict, str | None]:
@app.get("/api/calls")
def list_calls() -> Response:
"""Возвращает пагинированный список объектов calls."""
page = max(request.args.get("page", default=1, type=int), 1)
per_page = min(max(request.args.get("per_page", default=20, type=int), 1), 100)
offset = (page - 1) * per_page
@@ -115,6 +135,10 @@ def list_calls() -> Response:
@app.get("/api/calls/<int:call_id>")
def get_call(call_id: int) -> Response:
"""Возвращает один объект calls по его идентификатору."""
if call_id <= 0:
return format_response({"error": "id должен быть положительным целым числом"}, status=400)
conn = get_db_connection()
row = fetch_call_by_id(conn, call_id)
conn.close()
@@ -127,6 +151,7 @@ def get_call(call_id: int) -> Response:
@app.post("/api/calls")
def create_call() -> Response:
"""Создает новый объект calls из данных JSON-тела запроса."""
payload, error = parse_payload()
if error:
return format_response({"error": error}, status=400)
@@ -157,6 +182,10 @@ def create_call() -> Response:
@app.put("/api/calls/<int:call_id>")
def update_call(call_id: int) -> Response:
"""Обновляет существующий объект calls по id частичным набором полей."""
if call_id <= 0:
return format_response({"error": "id должен быть положительным целым числом"}, status=400)
payload, error = parse_payload()
if error:
return format_response({"error": error}, status=400)
@@ -184,6 +213,10 @@ def update_call(call_id: int) -> Response:
@app.delete("/api/calls/<int:call_id>")
def delete_call(call_id: int) -> Response:
"""Удаляет объект calls по id и возвращает статус операции."""
if call_id <= 0:
return format_response({"error": "id должен быть положительным целым числом"}, status=400)
conn = get_db_connection()
exists = fetch_call_by_id(conn, call_id)
if exists is None:
@@ -199,6 +232,7 @@ def delete_call(call_id: int) -> Response:
@app.get("/api/stats/hour/<int:hour>")
def stats_by_hour(hour: int) -> Response:
"""Возвращает количество обращений в таблице calls для заданного часа (0-23)."""
if hour < 0 or hour > 23:
return format_response({"error": "Час должен быть от 0 до 23"}, status=400)
@@ -215,8 +249,32 @@ def stats_by_hour(hour: int) -> Response:
return format_response(payload)
@app.route("/api/calls/<call_id>", methods=["GET", "PUT", "DELETE"])
def invalid_call_id(call_id: str) -> Response:
"""Возвращает понятную ошибку, если id в URL не является корректным числом."""
_, error = parse_positive_int(call_id, "id")
if error:
return format_response({"error": error}, status=400)
return format_response({"error": "Некорректный запрос"}, status=400)
@app.get("/api/stats/hour/<hour>")
def invalid_hour(hour: str) -> Response:
"""Возвращает понятную ошибку, если hour в URL задан некорректно."""
try:
parsed_hour = int(hour)
except (TypeError, ValueError):
return format_response({"error": "Час должен быть целым числом"}, status=400)
if parsed_hour < 0 or parsed_hour > 23:
return format_response({"error": "Час должен быть от 0 до 23"}, status=400)
return format_response({"error": "Некорректный запрос"}, status=400)
@app.get("/api/wsdl")
def wsdl_description() -> Response:
"""Отдает статическое WSDL-описание доступных операций сервиса."""
wsdl = """<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<definitions name=\"CallsService\"
targetNamespace=\"http://localhost:5000/calls\"
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.
Binary file not shown.