commit 83cb7d68b000358911357a497f8164c0be714bbc Author: Илья Глазунов Date: Mon Sep 1 23:49:50 2025 +0300 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d31fce4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.venv/ +venv/ + +config.example.yaml + +__pycache__/ + +logs/* + +static/* + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e24e2db --- /dev/null +++ b/README.md @@ -0,0 +1,270 @@ +# PyServe + +PyServe is a modern, async HTTP server written in Python. Originally created for educational purposes, it has evolved into a powerful tool for rapid prototyping and serving web applications with unique features like AI-generated content. + +isolated + +[More on web page](https://pyserve.org/) + +## Project Overview + +PyServe v0.4.2 introduces a completely refactored architecture with modern async/await syntax and new exciting features like **Vibe-Serving** - AI-powered dynamic content generation. + +### Key Features: + +- **Async HTTP Server** - Built with Python's asyncio for high performance +- **Advanced Configuration System V2** - Powerful extensible configuration with full backward compatibility +- **Regex Routing & SPA Support** - nginx-style routing patterns with Single Page Application fallback +- **Static File Serving** - Efficient serving with correct MIME types +- **Template System** - Dynamic content generation +- **Vibe-Serving Mode** - AI-generated content using language models (OpenAI, Claude, etc.) +- **Reverse Proxy** - Forward requests to backend services with advanced routing +- **SSL/HTTPS Support** - Secure connections with certificate configuration +- **Modular Extensions** - Plugin-like architecture for security, caching, monitoring +- **Beautiful Logging** - Colored terminal output with file rotation +- **Error Handling** - Styled error pages and graceful fallbacks + +## Getting Started + +### Prerequisites + +- Python 3.12 or higher +- Dependencies: `pip install -r requirements.txt` + +### Installation + +```bash +git clone https://github.com/ShiftyX1/PyServe.git +cd PyServe +pip install -r requirements.txt +``` + +### Running the Server + +Basic startup: +```bash +python run.py +``` + +Running with specific configuration: +```bash +python run.py -H 0.0.0.0 -p 8080 +``` + +**NEW: Vibe-Serving Mode (AI-Generated Content):** +```bash +python run.py --vibe-serving +``` + +### Command Line Options + +| Option | Description | +|--------|-------------| +| `-h, --help` | Show help and exit | +| `-c, --config CONFIG` | Path to configuration file | +| `-p, --port PORT` | Port to run the server on | +| `-H, --host HOST` | Host to bind the server to | +| `-s, --static STATIC` | Directory for static files | +| `-t, --templates TEMPLATES` | Directory for templates | +| `-v, --version` | Show version and exit | +| `-d, --debug` | Enable debug mode | +| `--ssl` | Enable SSL/HTTPS | +| `--cert CERT` | SSL certificate file | +| `--key KEY` | SSL private key file | +| `--proxy HOST:PORT/PATH` | Configure reverse proxy | +| `--vibe-serving` | **NEW:** Enable AI-generated content mode | +| `--skip-proxy-check` | Skip proxy availability check | + +## Vibe-Serving: AI-Generated Content + +PyServe v0.4.2 introduces **Vibe-Serving** - a revolutionary feature that generates web pages on-the-fly using AI language models. + +### How it works: +1. Configure routes and prompts in `vibeconfig.yaml` +2. Set your `OPENAI_API_KEY` environment variable +3. Start with `python run.py --vibe-serving` +4. Visit any configured route to see AI-generated content + +### Example vibeconfig.yaml: +```yaml +routes: + "/": "Generate a modern landing page for PyServe" + "/about": "Create an about page describing the project" + "/contact": "Generate a contact page with form" + +settings: + cache_ttl: 3600 + model: "gpt-4" + timeout: 30 +``` + +## Configuration + +PyServe supports two configuration formats with **full backward compatibility**: + +### V1 Configuration (Legacy - still supported) + +```yaml +server: + host: 127.0.0.1 + port: 8000 + backlog: 5 + +http: + static_dir: ./static + templates_dir: ./templates + +ssl: + enabled: false + +logging: + level: INFO +``` + +### V2 Configuration (Recommended) + +The new V2 configuration system adds powerful extensions while maintaining full V1 compatibility: + +```yaml +version: 2 + +# Core modules (same as V1) +server: + host: 0.0.0.0 + port: 8080 + +http: + static_dir: ./static + templates_dir: ./templates + +# NEW: Extensions system +extensions: + - type: routing + config: + regex_locations: + # API with version capture + "~^/api/v(?P\\d+)/": + proxy_pass: "http://backend:3000" + headers: + - "API-Version: {version}" + + # Static files with caching + "~*\\.(js|css|png|jpg)$": + root: "./static" + cache_control: "max-age=31536000" + + # SPA fallback + "__default__": + spa_fallback: true + root: "./dist" +``` + +#### Key V2 Features: + +- **Regex Routing** - nginx-style patterns with priorities +- **SPA Support** - Automatic fallback for Single Page Applications +- **Parameter Capture** - Extract URL parameters with named groups +- **External Modules** - Load extensions from separate files +- **Graceful Degradation** - Errors in extensions don't break core functionality + +📖 **[Complete V2 Configuration Guide](./CONFIGURATION_V2_GUIDE.md)** - Detailed documentation with examples + +### Quick V2 Examples + +#### Simple SPA Application +```yaml +version: 2 +server: + host: 0.0.0.0 + port: 8080 +http: + static_dir: ./static +extensions: + - type: routing + config: + regex_locations: + "~^/api/": { proxy_pass: "http://localhost:3000" } + "__default__": { spa_fallback: true, root: "./dist" } +``` + +#### Microservices Gateway +```yaml +version: 2 +extensions: + - type: routing + config: + regex_locations: + "~^/api/users/": { proxy_pass: "http://user-service:3001" } + "~^/api/orders/": { proxy_pass: "http://order-service:3002" } + "=/health": { return: "200 OK" } +``` + +### Main Configuration (config.yaml) + +```yaml +server: + host: 127.0.0.1 + port: 8000 + backlog: 5 + redirect_instructions: + - /home: /index.html + +http: + static_dir: ./static + templates_dir: ./templates + +ssl: + enabled: false + cert_file: ./ssl/cert.pem + key_file: ./ssl/key.pem + +logging: + level: INFO + log_file: ./logs/pyserve.log + console_output: true + use_colors: true +``` + +### Vibe Configuration (vibeconfig.yaml) + +For AI-generated content mode: + +```yaml +routes: + "/": "Create a beautiful landing page" + "/about": "Generate an about page" + +settings: + cache_ttl: 3600 + model: "gpt-4" + api_url: "https://api.openai.com/v1" # Optional custom endpoint +``` + +## Architecture + +PyServe v0.4.2 features a modular architecture: + +- **Core** - Base server components and configuration +- **HTTP** - Request/response handling and routing +- **Template** - Dynamic content rendering +- **Vibe** - AI-powered content generation +- **Utils** - Helper functions and utilities + +## Use Cases + +- **Modern Web Applications** - SPA hosting with API proxying +- **Microservices Gateway** - Route requests to multiple backend services +- **Development** - Quick local development server with hot-reload friendly routing +- **Prototyping** - Rapid testing with regex-based routing +- **Education** - Learning HTTP protocol, routing, and server architecture +- **AI Experimentation** - Testing AI-generated web content with Vibe-Serving +- **Static Sites** - Advanced static file serving with caching rules +- **Reverse Proxy** - Development and production proxy setup with pattern matching + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This project is distributed under the MIT license. \ No newline at end of file diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..2eb043d --- /dev/null +++ b/config.yaml @@ -0,0 +1,61 @@ +http: + static_dir: ./static + templates_dir: ./templates + +server: + host: 0.0.0.0 + port: 8080 + backlog: 5 + default_root: false + redirect_instructions: + - "/docs": "/docs.html" + +ssl: + enabled: false + cert_file: ./ssl/cert.pem + key_file: ./ssl/key.pem + +logging: + level: DEBUG + console_output: true + log_file: ./logs/pyserve.log + +# НОВОЕ: Расширяемые модули +extensions: + # Встроенное расширение для продвинутой маршрутизации + - type: routing + config: + regex_locations: + # API маршруты с захватом версии + "~^/api/v(?P\\d+)/": + proxy_pass: "http://localhost:9001" + headers: + - "API-Version: {version}" + - "X-Forwarded-For: $remote_addr" + + # Статические файлы с долгим кэшем + "~*\\.(js|css|png|jpg|gif|ico|svg|woff2?)$": + root: "./static" + cache_control: "public, max-age=31536000" + headers: + - "Access-Control-Allow-Origin: *" + + # Exact match для health check + "=/health": + return: "200 OK" + content_type: "text/plain" + + "=/": + root: "./static" + index_file: "index.html" + + # SPA fallback для всех остальных маршрутов + "__default__": + spa_fallback: true + root: "./static" + index_file: "docs.html" + # Исключения для SPA (не попадают в fallback) + exclude_patterns: + - "/api/" + - "/admin/" + - "/assets/" diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..212a51e --- /dev/null +++ b/poetry.lock @@ -0,0 +1,536 @@ +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. + +[[package]] +name = "anyio" +version = "4.10.0" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"}, + {file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "click" +version = "8.2.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "httptools" +version = "0.6.4" +description = "A collection of framework independent HTTP protocol utils." +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}, + {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}, + {file = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"}, + {file = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"}, + {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"}, + {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"}, + {file = "httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"}, + {file = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"}, + {file = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"}, + {file = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"}, + {file = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"}, + {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"}, + {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"}, + {file = "httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"}, + {file = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"}, + {file = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"}, + {file = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"}, + {file = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"}, + {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"}, + {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"}, + {file = "httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"}, + {file = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"}, + {file = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"}, + {file = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"}, + {file = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"}, + {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"}, + {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"}, + {file = "httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"}, + {file = "httptools-0.6.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba"}, + {file = "httptools-0.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc"}, + {file = "httptools-0.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff"}, + {file = "httptools-0.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490"}, + {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43"}, + {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440"}, + {file = "httptools-0.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f"}, + {file = "httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003"}, + {file = "httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab"}, + {file = "httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547"}, + {file = "httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9"}, + {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076"}, + {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd"}, + {file = "httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6"}, + {file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"}, +] + +[package.extras] +test = ["Cython (>=0.29.24)"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"}, + {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "starlette" +version = "0.47.3" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51"}, + {file = "starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" +typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version == \"3.12\"" +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "uvicorn" +version = "0.35.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a"}, + {file = "uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} +h11 = ">=0.8" +httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standard\""} +python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} +uvloop = {version = ">=0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} + +[package.extras] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "uvloop" +version = "0.21.0" +description = "Fast implementation of asyncio event loop on top of libuv" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"" +files = [ + {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, + {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, + {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26"}, + {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb"}, + {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"}, + {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"}, + {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8"}, + {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0"}, + {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e"}, + {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb"}, + {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6"}, + {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d"}, + {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c"}, + {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2"}, + {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d"}, + {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc"}, + {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb"}, + {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f"}, + {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281"}, + {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af"}, + {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6"}, + {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816"}, + {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc"}, + {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553"}, + {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414"}, + {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206"}, + {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe"}, + {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79"}, + {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a"}, + {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc"}, + {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b"}, + {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2"}, + {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0"}, + {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75"}, + {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd"}, + {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff"}, + {file = "uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3"}, +] + +[package.extras] +dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] + +[[package]] +name = "watchfiles" +version = "1.1.0" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "watchfiles-1.1.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc"}, + {file = "watchfiles-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df"}, + {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68"}, + {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc"}, + {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97"}, + {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c"}, + {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5"}, + {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9"}, + {file = "watchfiles-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72"}, + {file = "watchfiles-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc"}, + {file = "watchfiles-1.1.0-cp310-cp310-win32.whl", hash = "sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587"}, + {file = "watchfiles-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82"}, + {file = "watchfiles-1.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2"}, + {file = "watchfiles-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c"}, + {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d"}, + {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7"}, + {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c"}, + {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575"}, + {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8"}, + {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f"}, + {file = "watchfiles-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4"}, + {file = "watchfiles-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d"}, + {file = "watchfiles-1.1.0-cp311-cp311-win32.whl", hash = "sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2"}, + {file = "watchfiles-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12"}, + {file = "watchfiles-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a"}, + {file = "watchfiles-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179"}, + {file = "watchfiles-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5"}, + {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297"}, + {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0"}, + {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e"}, + {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee"}, + {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd"}, + {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f"}, + {file = "watchfiles-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4"}, + {file = "watchfiles-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f"}, + {file = "watchfiles-1.1.0-cp312-cp312-win32.whl", hash = "sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd"}, + {file = "watchfiles-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47"}, + {file = "watchfiles-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6"}, + {file = "watchfiles-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30"}, + {file = "watchfiles-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a"}, + {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc"}, + {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b"}, + {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895"}, + {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a"}, + {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b"}, + {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c"}, + {file = "watchfiles-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b"}, + {file = "watchfiles-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb"}, + {file = "watchfiles-1.1.0-cp313-cp313-win32.whl", hash = "sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9"}, + {file = "watchfiles-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7"}, + {file = "watchfiles-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5"}, + {file = "watchfiles-1.1.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1"}, + {file = "watchfiles-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339"}, + {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633"}, + {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011"}, + {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670"}, + {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf"}, + {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4"}, + {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20"}, + {file = "watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef"}, + {file = "watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb"}, + {file = "watchfiles-1.1.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297"}, + {file = "watchfiles-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018"}, + {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0"}, + {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12"}, + {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb"}, + {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77"}, + {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92"}, + {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e"}, + {file = "watchfiles-1.1.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b"}, + {file = "watchfiles-1.1.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259"}, + {file = "watchfiles-1.1.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f"}, + {file = "watchfiles-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e"}, + {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa"}, + {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8"}, + {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f"}, + {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e"}, + {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb"}, + {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147"}, + {file = "watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8"}, + {file = "watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db"}, + {file = "watchfiles-1.1.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa"}, + {file = "watchfiles-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433"}, + {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4"}, + {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7"}, + {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f"}, + {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf"}, + {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29"}, + {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e"}, + {file = "watchfiles-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86"}, + {file = "watchfiles-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f"}, + {file = "watchfiles-1.1.0-cp39-cp39-win32.whl", hash = "sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267"}, + {file = "watchfiles-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc"}, + {file = "watchfiles-1.1.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5"}, + {file = "watchfiles-1.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d"}, + {file = "watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea"}, + {file = "watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6"}, + {file = "watchfiles-1.1.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3"}, + {file = "watchfiles-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c"}, + {file = "watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432"}, + {file = "watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792"}, + {file = "watchfiles-1.1.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9"}, + {file = "watchfiles-1.1.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a"}, + {file = "watchfiles-1.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866"}, + {file = "watchfiles-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277"}, + {file = "watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575"}, +] + +[package.dependencies] +anyio = ">=3.0.0" + +[[package]] +name = "websockets" +version = "15.0.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, + {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, + {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, + {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, + {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, + {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, + {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, + {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, + {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, + {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, + {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, + {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, + {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, +] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.12" +content-hash = "056f0e4f1dd06e7d3a7fb2e5c6a891791c1cb18ce77466c4306de7cc8242cb73" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f283f19 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "pyserve" +version = "0.6.0" +description = "Simple HTTP Web server written in Python" +authors = [ + {name = "Илья Глазунов",email = "i.glazunov@sapiens.solutions"} +] +license = {text = "MIT"} +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "starlette (>=0.47.3,<0.48.0)", + "uvicorn[standard] (>=0.35.0,<0.36.0)", + "pyyaml (>=6.0,<7.0)" +] + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/pyserve/__init__.py b/pyserve/__init__.py new file mode 100644 index 0000000..5342f88 --- /dev/null +++ b/pyserve/__init__.py @@ -0,0 +1,11 @@ +""" +PyServe - HTTP веб-сервер с функционалом nginx +""" + +__version__ = "0.6.0" +__author__ = "Илья Глазунов" + +from .server import PyServeServer +from .config import Config + +__all__ = ["PyServeServer", "Config"] diff --git a/pyserve/config.py b/pyserve/config.py new file mode 100644 index 0000000..e9a074f --- /dev/null +++ b/pyserve/config.py @@ -0,0 +1,155 @@ +import yaml +import os +from pathlib import Path +from typing import Dict, Any, List, Optional +from dataclasses import dataclass, field +import logging +from .logging_utils import setup_logging + + +@dataclass +class HttpConfig: + static_dir: str = "./static" + templates_dir: str = "./templates" + + +@dataclass +class ServerConfig: + host: str = "0.0.0.0" + port: int = 8080 + backlog: int = 5 + default_root: bool = False + redirect_instructions: Dict[str, str] = field(default_factory=dict) + + +@dataclass +class SSLConfig: + enabled: bool = False + cert_file: str = "./ssl/cert.pem" + key_file: str = "./ssl/key.pem" + + +@dataclass +class LoggingConfig: + level: str = "INFO" + console_output: bool = True + log_file: str = "./logs/pyserve.log" + + +@dataclass +class RoutingExtensionConfig: + regex_locations: Dict[str, Dict[str, Any]] = field(default_factory=dict) + + +@dataclass +class ExtensionConfig: + type: str + config: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class Config: + http: HttpConfig = field(default_factory=HttpConfig) + server: ServerConfig = field(default_factory=ServerConfig) + ssl: SSLConfig = field(default_factory=SSLConfig) + logging: LoggingConfig = field(default_factory=LoggingConfig) + extensions: List[ExtensionConfig] = field(default_factory=list) + + @classmethod + def from_yaml(cls, file_path: str) -> "Config": + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = yaml.safe_load(f) + + return cls._from_dict(data) + except FileNotFoundError: + logging.warning(f"Конфигурационный файл {file_path} не найден. Используются значения по умолчанию.") + return cls() + except yaml.YAMLError as e: + logging.error(f"Ошибка парсинга YAML файла {file_path}: {e}") + raise + + @classmethod + def _from_dict(cls, data: Dict[str, Any]) -> "Config": + config = cls() + + if 'http' in data: + http_data = data['http'] + config.http = HttpConfig( + static_dir=http_data.get('static_dir', config.http.static_dir), + templates_dir=http_data.get('templates_dir', config.http.templates_dir) + ) + + if 'server' in data: + server_data = data['server'] + config.server = ServerConfig( + host=server_data.get('host', config.server.host), + port=server_data.get('port', config.server.port), + backlog=server_data.get('backlog', config.server.backlog), + default_root=server_data.get('default_root', config.server.default_root), + redirect_instructions=server_data.get('redirect_instructions', {}) + ) + + if 'ssl' in data: + ssl_data = data['ssl'] + config.ssl = SSLConfig( + enabled=ssl_data.get('enabled', config.ssl.enabled), + cert_file=ssl_data.get('cert_file', config.ssl.cert_file), + key_file=ssl_data.get('key_file', config.ssl.key_file) + ) + + if 'logging' in data: + log_data = data['logging'] + config.logging = LoggingConfig( + level=log_data.get('level', config.logging.level), + console_output=log_data.get('console_output', config.logging.console_output), + log_file=log_data.get('log_file', config.logging.log_file) + ) + + if 'extensions' in data: + for ext_data in data['extensions']: + extension = ExtensionConfig( + type=ext_data.get('type', ''), + config=ext_data.get('config', {}) + ) + config.extensions.append(extension) + + return config + + def validate(self) -> bool: + errors = [] + + if not os.path.exists(self.http.static_dir): + errors.append(f"Статическая директория не существует: {self.http.static_dir}") + + if self.ssl.enabled: + if not os.path.exists(self.ssl.cert_file): + errors.append(f"SSL сертификат не найден: {self.ssl.cert_file}") + if not os.path.exists(self.ssl.key_file): + errors.append(f"SSL ключ не найден: {self.ssl.key_file}") + + if not (1 <= self.server.port <= 65535): + errors.append(f"Некорректный порт: {self.server.port}") + + log_dir = os.path.dirname(self.logging.log_file) + if log_dir and not os.path.exists(log_dir): + try: + os.makedirs(log_dir, exist_ok=True) + except OSError as e: + errors.append(f"Невозможно создать директорию для логов: {e}") + + if errors: + for error in errors: + logging.error(f"Ошибка конфигурации: {error}") + return False + + return True + + def setup_logging(self) -> None: + """Настройка системы логирования через кастомный менеджер""" + config_dict = { + 'level': self.logging.level, + 'console_output': self.logging.console_output, + 'log_file': self.logging.log_file + } + setup_logging(config_dict) diff --git a/pyserve/extensions.py b/pyserve/extensions.py new file mode 100644 index 0000000..796c516 --- /dev/null +++ b/pyserve/extensions.py @@ -0,0 +1,200 @@ +from abc import ABC, abstractmethod +from typing import Dict, Any, List, Optional +from starlette.requests import Request +from starlette.responses import Response +from .logging_utils import get_logger + +logger = get_logger(__name__) + + +class Extension(ABC): + def __init__(self, config: Dict[str, Any]): + self.config = config + self.enabled = True + + @abstractmethod + async def process_request(self, request: Request) -> Optional[Response]: + pass + + @abstractmethod + async def process_response(self, request: Request, response: Response) -> Response: + pass + + def initialize(self) -> None: + pass + + def cleanup(self) -> None: + pass + + +class RoutingExtension(Extension): + def __init__(self, config: Dict[str, Any]): + super().__init__(config) + from .routing import create_router_from_config + + regex_locations = config.get("regex_locations", {}) + self.router = create_router_from_config(regex_locations) + from .routing import RequestHandler + self.handler = RequestHandler(self.router) + + async def process_request(self, request: Request) -> Optional[Response]: + try: + return await self.handler.handle(request) + except Exception as e: + logger.error(f"Ошибка в RoutingExtension: {e}") + return None + + async def process_response(self, request: Request, response: Response) -> Response: + return response + + +class SecurityExtension(Extension): + def __init__(self, config: Dict[str, Any]): + super().__init__(config) + self.allowed_ips = config.get("allowed_ips", []) + self.blocked_ips = config.get("blocked_ips", []) + self.security_headers = config.get("security_headers", { + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + "X-XSS-Protection": "1; mode=block" + }) + + async def process_request(self, request: Request) -> Optional[Response]: + client_ip = request.client.host if request.client else "unknown" + + if self.blocked_ips and client_ip in self.blocked_ips: + logger.warning(f"Заблокирован запрос от IP: {client_ip}") + from starlette.responses import PlainTextResponse + return PlainTextResponse("403 Forbidden", status_code=403) + + if self.allowed_ips and client_ip not in self.allowed_ips: + logger.warning(f"Запрещен доступ для IP: {client_ip}") + from starlette.responses import PlainTextResponse + return PlainTextResponse("403 Forbidden", status_code=403) + + return None + + async def process_response(self, request: Request, response: Response) -> Response: + for header, value in self.security_headers.items(): + response.headers[header] = value + return response + + +class CachingExtension(Extension): + def __init__(self, config: Dict[str, Any]): + super().__init__(config) + self.cache: Dict[str, Any] = {} + self.cache_patterns = config.get("cache_patterns", []) + self.cache_ttl = config.get("cache_ttl", 3600) + + async def process_request(self, request: Request) -> Optional[Response]: + # TODO: Реализовать проверку кэша + return None + + async def process_response(self, request: Request, response: Response) -> Response: + # TODO: Реализовать кэширование ответов + return response + + +class MonitoringExtension(Extension): + def __init__(self, config: Dict[str, Any]): + super().__init__(config) + self.request_count = 0 + self.error_count = 0 + self.response_times = [] + self.enable_metrics = config.get("enable_metrics", True) + + async def process_request(self, request: Request) -> Optional[Response]: + if self.enable_metrics: + self.request_count += 1 + request.state.start_time = __import__('time').time() + return None + + async def process_response(self, request: Request, response: Response) -> Response: + if self.enable_metrics and hasattr(request.state, 'start_time'): + response_time = __import__('time').time() - request.state.start_time + self.response_times.append(response_time) + + if response.status_code >= 400: + self.error_count += 1 + + logger.info(f"Request: {request.method} {request.url.path} - " + f"Status: {response.status_code} - " + f"Time: {response_time:.3f}s") + + return response + + def get_metrics(self) -> Dict[str, Any]: + avg_response_time = (sum(self.response_times) / len(self.response_times) + if self.response_times else 0) + + return { + "request_count": self.request_count, + "error_count": self.error_count, + "error_rate": self.error_count / max(self.request_count, 1), + "avg_response_time": avg_response_time, + "total_response_times": len(self.response_times) + } + + +class ExtensionManager: + def __init__(self): + self.extensions: List[Extension] = [] + self.extension_registry = { + "routing": RoutingExtension, + "security": SecurityExtension, + "caching": CachingExtension, + "monitoring": MonitoringExtension + } + + def register_extension_type(self, name: str, extension_class: type) -> None: + self.extension_registry[name] = extension_class + + def load_extension(self, extension_type: str, config: Dict[str, Any]) -> None: + if extension_type not in self.extension_registry: + logger.error(f"Неизвестный тип расширения: {extension_type}") + return + + try: + extension_class = self.extension_registry[extension_type] + extension = extension_class(config) + extension.initialize() + self.extensions.append(extension) + logger.info(f"Загружено расширение: {extension_type}") + except Exception as e: + logger.error(f"Ошибка загрузки расширения {extension_type}: {e}") + + async def process_request(self, request: Request) -> Optional[Response]: + for extension in self.extensions: + if not extension.enabled: + continue + + try: + response = await extension.process_request(request) + if response is not None: + return response + except Exception as e: + logger.error(f"Ошибка в расширении {type(extension).__name__}: {e}") + + return None + + async def process_response(self, request: Request, response: Response) -> Response: + for extension in self.extensions: + if not extension.enabled: + continue + + try: + response = await extension.process_response(request, response) + except Exception as e: + logger.error(f"Ошибка в расширении {type(extension).__name__}: {e}") + + return response + + def cleanup(self) -> None: + for extension in self.extensions: + try: + extension.cleanup() + except Exception as e: + logger.error(f"Ошибка при очистке расширения {type(extension).__name__}: {e}") + + self.extensions.clear() diff --git a/pyserve/logging_utils.py b/pyserve/logging_utils.py new file mode 100644 index 0000000..f481063 --- /dev/null +++ b/pyserve/logging_utils.py @@ -0,0 +1,279 @@ +""" +Кастомная система логирования для PyServe +Управляет логгерами всех пакетов и модулей, включая uvicorn и starlette +""" + +import logging +import logging.handlers +import sys +import time +from pathlib import Path +from typing import Dict, Any, List + +from . import __version__ + + +class UvicornLogFilter(logging.Filter): + def filter(self, record): + if hasattr(record, 'name') and 'uvicorn.access' in record.name: + if hasattr(record, 'getMessage'): + msg = record.getMessage() + if ' - "' in msg and '" ' in msg: + parts = msg.split(' - "') + if len(parts) >= 2: + client_info = parts[0] + request_part = parts[1].split('" ') + if len(request_part) >= 2: + method_path = request_part[0] + status_part = request_part[1] + record.msg = f"Access: {client_info} - {method_path} - {status_part}" + + return True + + +class PyServeFormatter(logging.Formatter): + COLORS = { + 'DEBUG': '\033[36m', # Cyan + 'INFO': '\033[32m', # Green + 'WARNING': '\033[33m', # Yellow + 'ERROR': '\033[31m', # Red + 'CRITICAL': '\033[35m', # Magenta + 'RESET': '\033[0m' # Reset + } + + def __init__(self, use_colors: bool = True, show_module: bool = True, *args, **kwargs): + super().__init__(*args, **kwargs) + self.use_colors = use_colors and hasattr(sys.stderr, 'isatty') and sys.stderr.isatty() + self.show_module = show_module + + def format(self, record): + if self.use_colors: + levelname = record.levelname + if levelname in self.COLORS: + record.levelname = f"{self.COLORS[levelname]}{levelname}{self.COLORS['RESET']}" + + if self.show_module and hasattr(record, 'name'): + name = record.name + if name.startswith('uvicorn'): + record.name = 'uvicorn' + elif name.startswith('pyserve'): + pass + elif name.startswith('starlette'): + record.name = 'starlette' + + return super().format(record) + + +class AccessLogHandler(logging.Handler): + def __init__(self, logger_name: str = 'pyserve.access'): + super().__init__() + self.access_logger = logging.getLogger(logger_name) + + def emit(self, record): + self.access_logger.handle(record) + + +class PyServeLogManager: + def __init__(self): + self.configured = False + self.handlers: Dict[str, logging.Handler] = {} + self.loggers: Dict[str, logging.Logger] = {} + self.original_handlers: Dict[str, List[logging.Handler]] = {} + + def setup_logging(self, config: Dict[str, Any]) -> None: + if self.configured: + return + + level = config.get('level', 'INFO').upper() + console_output = config.get('console_output', True) + log_file = config.get('log_file', './logs/pyserve.log') + self._save_original_handlers() + self._clear_all_handlers() + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + + detailed_formatter = PyServeFormatter( + use_colors=False, + show_module=True, + fmt='%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s' + ) + + console_formatter = PyServeFormatter( + use_colors=True, + show_module=True, + fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + if console_output: + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(getattr(logging, level)) + console_handler.setFormatter(console_formatter) + + console_handler.addFilter(UvicornLogFilter()) + + root_logger.addHandler(console_handler) + self.handlers['console'] = console_handler + + if log_file: + self._ensure_log_directory(log_file) + + file_handler = logging.handlers.RotatingFileHandler( + log_file, + maxBytes=10*1024*1024, # 10MB + backupCount=5, + encoding='utf-8' + ) + file_handler.setLevel(logging.DEBUG) + file_handler.setFormatter(detailed_formatter) + file_handler.addFilter(UvicornLogFilter()) + + root_logger.addHandler(file_handler) + self.handlers['file'] = file_handler + + self._configure_library_loggers(level) + + self._intercept_uvicorn_logging() + + pyserve_logger = logging.getLogger('pyserve') + pyserve_logger.setLevel(getattr(logging, level)) + self.loggers['pyserve'] = pyserve_logger + + pyserve_logger.info(f"PyServe v{__version__} - Система логирования инициализирована") + pyserve_logger.info(f"Уровень логирования: {level}") + pyserve_logger.info(f"Консольный вывод: {'включен' if console_output else 'отключен'}") + pyserve_logger.info(f"Файл логов: {log_file if log_file else 'отключен'}") + + self.configured = True + + def _save_original_handlers(self) -> None: + logger_names = ['', 'uvicorn', 'uvicorn.access', 'uvicorn.error', 'starlette'] + + for name in logger_names: + logger = logging.getLogger(name) + self.original_handlers[name] = logger.handlers.copy() + + def _clear_all_handlers(self) -> None: + root_logger = logging.getLogger() + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + handler.close() + + logger_names = ['uvicorn', 'uvicorn.access', 'uvicorn.error', 'starlette'] + for name in logger_names: + logger = logging.getLogger(name) + for handler in logger.handlers[:]: + logger.removeHandler(handler) + handler.close() + + self.handlers.clear() + + def _ensure_log_directory(self, log_file: str) -> None: + log_dir = Path(log_file).parent + log_dir.mkdir(parents=True, exist_ok=True) + + def _configure_library_loggers(self, main_level: str) -> None: + library_configs = { + # Uvicorn и связанные - только в DEBUG режиме + 'uvicorn': 'DEBUG' if main_level == 'DEBUG' else 'WARNING', + 'uvicorn.access': 'DEBUG' if main_level == 'DEBUG' else 'WARNING', + 'uvicorn.error': 'DEBUG' if main_level == 'DEBUG' else 'ERROR', + 'uvicorn.asgi': 'DEBUG' if main_level == 'DEBUG' else 'WARNING', + + # Starlette - только в DEBUG режиме + 'starlette': 'DEBUG' if main_level == 'DEBUG' else 'WARNING', + + 'asyncio': 'WARNING', + 'concurrent.futures': 'WARNING', + 'multiprocessing': 'WARNING', + 'pyserve': main_level, + 'pyserve.server': main_level, + 'pyserve.routing': main_level, + 'pyserve.extensions': main_level, + 'pyserve.config': main_level, + } + + for logger_name, level in library_configs.items(): + logger = logging.getLogger(logger_name) + logger.setLevel(getattr(logging, level)) + if logger_name.startswith('uvicorn') and logger_name != 'uvicorn': + logger.propagate = False + self.loggers[logger_name] = logger + + def _intercept_uvicorn_logging(self) -> None: + uvicorn_logger = logging.getLogger('uvicorn') + uvicorn_access_logger = logging.getLogger('uvicorn.access') + + for handler in uvicorn_logger.handlers[:]: + uvicorn_logger.removeHandler(handler) + + for handler in uvicorn_access_logger.handlers[:]: + uvicorn_access_logger.removeHandler(handler) + + uvicorn_logger.propagate = True + uvicorn_access_logger.propagate = True + + def get_logger(self, name: str) -> logging.Logger: + if name not in self.loggers: + logger = logging.getLogger(name) + self.loggers[name] = logger + + return self.loggers[name] + + def set_level(self, logger_name: str, level: str) -> None: + if logger_name in self.loggers: + self.loggers[logger_name].setLevel(getattr(logging, level.upper())) + + def add_handler(self, name: str, handler: logging.Handler) -> None: + if name not in self.handlers: + root_logger = logging.getLogger() + root_logger.addHandler(handler) + self.handlers[name] = handler + + def remove_handler(self, name: str) -> None: + if name in self.handlers: + root_logger = logging.getLogger() + root_logger.removeHandler(self.handlers[name]) + self.handlers[name].close() + del self.handlers[name] + + def create_access_log(self, method: str, path: str, status_code: int, + response_time: float, client_ip: str, user_agent: str = "") -> None: + access_logger = self.get_logger('pyserve.access') + + log_message = f'{client_ip} - - [{time.strftime("%d/%b/%Y:%H:%M:%S %z")}] ' \ + f'"{method} {path} HTTP/1.1" {status_code} - ' \ + f'"{user_agent}" {response_time:.3f}s' + + access_logger.info(log_message) + + def shutdown(self) -> None: + for handler in self.handlers.values(): + handler.close() + self.handlers.clear() + + for logger_name, handlers in self.original_handlers.items(): + logger = logging.getLogger(logger_name) + for handler in handlers: + logger.addHandler(handler) + + self.loggers.clear() + self.configured = False + +log_manager = PyServeLogManager() + + +def setup_logging(config: Dict[str, Any]) -> None: + log_manager.setup_logging(config) + + +def get_logger(name: str) -> logging.Logger: + return log_manager.get_logger(name) + + +def create_access_log(method: str, path: str, status_code: int, + response_time: float, client_ip: str, user_agent: str = "") -> None: + log_manager.create_access_log(method, path, status_code, response_time, client_ip, user_agent) + + +def shutdown_logging() -> None: + log_manager.shutdown() diff --git a/pyserve/routing.py b/pyserve/routing.py new file mode 100644 index 0000000..742e2b2 --- /dev/null +++ b/pyserve/routing.py @@ -0,0 +1,183 @@ +import re +import mimetypes +from pathlib import Path +from typing import Dict, Any, Optional, Pattern +from starlette.requests import Request +from starlette.responses import Response, FileResponse, PlainTextResponse +from .logging_utils import get_logger + +logger = get_logger(__name__) + +class RouteMatch: + def __init__(self, config: Dict[str, Any], params: Optional[Dict[str, str]] = None): + self.config = config + self.params = params or {} + +class Router: + def __init__(self, static_dir: str = "./static"): + self.static_dir = Path(static_dir) + self.routes: Dict[Pattern, Dict[str, Any]] = {} + self.exact_routes: Dict[str, Dict[str, Any]] = {} + self.default_route: Optional[Dict[str, Any]] = None + + def add_route(self, pattern: str, config: Dict[str, Any]) -> None: + if pattern.startswith("="): + exact_path = pattern[1:] + self.exact_routes[exact_path] = config + logger.debug(f"Добавлен exact маршрут: {exact_path}") + return + + if pattern == "__default__": + self.default_route = config + logger.debug("Добавлен default маршрут") + return + + if pattern.startswith("~"): + case_insensitive = pattern.startswith("~*") + regex_pattern = pattern[2:] if case_insensitive else pattern[1:] + + flags = re.IGNORECASE if case_insensitive else 0 + try: + compiled_pattern = re.compile(regex_pattern, flags) + self.routes[compiled_pattern] = config + logger.debug(f"Добавлен regex маршрут: {pattern}") + except re.error as e: + logger.error(f"Ошибка компиляции regex {pattern}: {e}") + + def match(self, path: str) -> Optional[RouteMatch]: + if path in self.exact_routes: + return RouteMatch(self.exact_routes[path]) + + for pattern, config in self.routes.items(): + match = pattern.search(path) + if match: + params = match.groupdict() + return RouteMatch(config, params) + + if self.default_route: + return RouteMatch(self.default_route) + + return None + + +class RequestHandler: + def __init__(self, router: Router, static_dir: str = "./static"): + self.router = router + self.static_dir = Path(static_dir) + + async def handle(self, request: Request) -> Response: + path = request.url.path + + logger.info(f"{request.method} {path}") + + route_match = self.router.match(path) + if not route_match: + return PlainTextResponse("404 Not Found", status_code=404) + + try: + return await self._process_route(request, route_match) + except Exception as e: + logger.error(f"Ошибка обработки запроса {path}: {e}") + return PlainTextResponse("500 Internal Server Error", status_code=500) + + async def _process_route(self, request: Request, route_match: RouteMatch) -> Response: + config = route_match.config + path = request.url.path + + if "return" in config: + status_text = config["return"] + if " " in status_text: + status_code, text = status_text.split(" ", 1) + status_code = int(status_code) + else: + status_code = int(status_text) + text = "" + + content_type = config.get("content_type", "text/plain") + return PlainTextResponse(text, status_code=status_code, + media_type=content_type) + + if "proxy_pass" in config: + return await self._handle_proxy(request, config, route_match.params) + + if "root" in config: + return await self._handle_static(request, config) + + if config.get("spa_fallback"): + return await self._handle_spa_fallback(request, config) + + return PlainTextResponse("404 Not Found", status_code=404) + + async def _handle_static(self, request: Request, config: Dict[str, Any]) -> Response: + root = Path(config["root"]) + path = request.url.path.lstrip("/") + + if not path or path == "/": + index_file = config.get("index_file", "index.html") + file_path = root / index_file + else: + file_path = root / path + + try: + file_path = file_path.resolve() + root = root.resolve() + if not str(file_path).startswith(str(root)): + return PlainTextResponse("403 Forbidden", status_code=403) + except OSError: + return PlainTextResponse("404 Not Found", status_code=404) + + if not file_path.exists() or not file_path.is_file(): + return PlainTextResponse("404 Not Found", status_code=404) + + content_type, _ = mimetypes.guess_type(str(file_path)) + + response = FileResponse(str(file_path), media_type=content_type) + + if "headers" in config: + for header in config["headers"]: + if ":" in header: + name, value = header.split(":", 1) + response.headers[name.strip()] = value.strip() + + if "cache_control" in config: + response.headers["Cache-Control"] = config["cache_control"] + + return response + + async def _handle_spa_fallback(self, request: Request, config: Dict[str, Any]) -> Response: + path = request.url.path + + exclude_patterns = config.get("exclude_patterns", []) + for pattern in exclude_patterns: + if path.startswith(pattern): + return PlainTextResponse("404 Not Found", status_code=404) + + root = Path(config.get("root", self.static_dir)) + index_file = config.get("index_file", "index.html") + file_path = root / index_file + + if file_path.exists() and file_path.is_file(): + return FileResponse(str(file_path), media_type="text/html") + + return PlainTextResponse("404 Not Found", status_code=404) + + async def _handle_proxy(self, request: Request, config: Dict[str, Any], + params: Dict[str, str]) -> Response: + # TODO: Реализовать полноценное проксирование + proxy_url = config["proxy_pass"] + + for key, value in params.items(): + proxy_url = proxy_url.replace(f"{{{key}}}", value) + + logger.info(f"Проксирование запроса на: {proxy_url}") + + return PlainTextResponse(f"Proxy to: {proxy_url}", status_code=200) + + +def create_router_from_config(regex_locations: Dict[str, Dict[str, Any]]) -> Router: + router = Router() + + for pattern, config in regex_locations.items(): + router.add_route(pattern, config) + + return router diff --git a/pyserve/server.py b/pyserve/server.py new file mode 100644 index 0000000..103f739 --- /dev/null +++ b/pyserve/server.py @@ -0,0 +1,239 @@ +import ssl +import uvicorn +import time +from starlette.applications import Starlette +from starlette.requests import Request +from starlette.responses import Response, PlainTextResponse +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.routing import Route +from pathlib import Path +from typing import Optional, Dict, Any + +from .config import Config +from .extensions import ExtensionManager +from .logging_utils import get_logger +from . import __version__ + +logger = get_logger(__name__) + + +class PyServeMiddleware(BaseHTTPMiddleware): + def __init__(self, app, extension_manager: ExtensionManager): + super().__init__(app) + self.extension_manager = extension_manager + self.access_logger = get_logger('pyserve.access') + + async def dispatch(self, request: Request, call_next): + start_time = time.time() + response = await self.extension_manager.process_request(request) + + if response is None: + response = await call_next(request) + + response = await self.extension_manager.process_response(request, response) + + response.headers["Server"] = f"pyserve/{__version__}" + client_ip = request.client.host if request.client else "unknown" + method = request.method + path = str(request.url.path) + query = str(request.url.query) if request.url.query else "" + if query: + path += f"?{query}" + status_code = response.status_code + process_time = round((time.time() - start_time) * 1000, 2) + + self.access_logger.info(f"{client_ip} - {method} {path} - {status_code} - {process_time}ms") + + return response + + +class PyServeServer: + def __init__(self, config: Config): + self.config = config + self.extension_manager = ExtensionManager() + self.app: Optional[Starlette] = None + self._setup_logging() + self._load_extensions() + self._create_app() + + def _setup_logging(self) -> None: + self.config.setup_logging() + logger.info("PyServe сервер инициализирован") + + def _load_extensions(self) -> None: + for ext_config in self.config.extensions: + self.extension_manager.load_extension( + ext_config.type, + ext_config.config + ) + + def _create_app(self) -> None: + routes = [ + Route("/health", self._health_check, methods=["GET"]), + Route("/metrics", self._metrics, methods=["GET"]), + Route("/{path:path}", self._catch_all, methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]), + ] + + self.app = Starlette(routes=routes) + self.app.add_middleware(PyServeMiddleware, extension_manager=self.extension_manager) + + async def _health_check(self, request: Request) -> Response: + return PlainTextResponse("OK", status_code=200) + + async def _metrics(self, request: Request) -> Response: + metrics = {} + + for extension in self.extension_manager.extensions: + if hasattr(extension, 'get_metrics'): + try: + ext_metrics = getattr(extension, 'get_metrics')() + metrics.update(ext_metrics) + except Exception as e: + logger.error(f"Ошибка получения метрик от {type(extension).__name__}: {e}") + + import json + return Response( + json.dumps(metrics, ensure_ascii=False, indent=2), + media_type="application/json" + ) + + async def _catch_all(self, request: Request) -> Response: + return PlainTextResponse("404 Not Found", status_code=404) + + def _create_ssl_context(self) -> Optional[ssl.SSLContext]: + if not self.config.ssl.enabled: + return None + + if not Path(self.config.ssl.cert_file).exists(): + logger.error(f"SSL сертификат не найден: {self.config.ssl.cert_file}") + return None + + if not Path(self.config.ssl.key_file).exists(): + logger.error(f"SSL ключ не найден: {self.config.ssl.key_file}") + return None + + try: + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + context.load_cert_chain( + self.config.ssl.cert_file, + self.config.ssl.key_file + ) + logger.info("SSL контекст создан успешно") + return context + except Exception as e: + logger.error(f"Ошибка создания SSL контекста: {e}") + return None + + def run(self) -> None: + """Запуск сервера""" + if not self.config.validate(): + logger.error("Конфигурация невалидна, сервер не может быть запущен") + return + + # Создаем директории если их нет + self._ensure_directories() + + # SSL конфигурация + ssl_context = self._create_ssl_context() + + uvicorn_config = { + "app": self.app, + "host": self.config.server.host, + "port": self.config.server.port, + "log_level": "critical", + "access_log": False, + "use_colors": False, + "server_header": False, + } + + if ssl_context: + uvicorn_config.update({ + "ssl_keyfile": self.config.ssl.key_file, + "ssl_certfile": self.config.ssl.cert_file, + }) + protocol = "https" + else: + protocol = "http" + + logger.info(f"Запуск PyServe сервера на {protocol}://{self.config.server.host}:{self.config.server.port}") + + try: + uvicorn.run(**uvicorn_config) + except KeyboardInterrupt: + logger.info("Получен сигнал остановки") + except Exception as e: + logger.error(f"Ошибка запуска сервера: {e}") + finally: + self.shutdown() + + async def run_async(self) -> None: + if not self.config.validate(): + logger.error("Конфигурация невалидна, сервер не может быть запущен") + return + + self._ensure_directories() + + config = uvicorn.Config( + app=self.app, # type: ignore + host=self.config.server.host, + port=self.config.server.port, + log_level="critical", + access_log=False, + use_colors=False, + ) + + server = uvicorn.Server(config) + + try: + await server.serve() + finally: + self.shutdown() + + def _ensure_directories(self) -> None: + directories = [ + self.config.http.static_dir, + self.config.http.templates_dir, + ] + + log_dir = Path(self.config.logging.log_file).parent + if log_dir != Path("."): + directories.append(str(log_dir)) + + for directory in directories: + Path(directory).mkdir(parents=True, exist_ok=True) + logger.debug(f"Создана/проверена директория: {directory}") + + def shutdown(self) -> None: + logger.info("Завершение работы PyServe сервера") + self.extension_manager.cleanup() + + from .logging_utils import shutdown_logging + shutdown_logging() + + logger.info("Сервер остановлен") + + def add_extension(self, extension_type: str, config: Dict[str, Any]) -> None: + self.extension_manager.load_extension(extension_type, config) + + def get_metrics(self) -> Dict[str, Any]: + metrics = {"server_status": "running"} + + for extension in self.extension_manager.extensions: + if hasattr(extension, 'get_metrics'): + try: + ext_metrics = getattr(extension, 'get_metrics')() + metrics.update(ext_metrics) + except Exception as e: + logger.error(f"Ошибка получения метрик от {type(extension).__name__}: {e}") + + return metrics + + +def create_server(config_path: str = "config.yaml") -> PyServeServer: + config = Config.from_yaml(config_path) + return PyServeServer(config) + + +def run_server(config_path: str = "config.yaml") -> None: + server = create_server(config_path) + server.run() diff --git a/run.py b/run.py new file mode 100644 index 0000000..5bd799a --- /dev/null +++ b/run.py @@ -0,0 +1,63 @@ +import sys +import argparse +from pathlib import Path + +from pyserve import PyServeServer, Config + + +def main(): + parser = argparse.ArgumentParser(description="PyServe - HTTP веб-сервер") + parser.add_argument( + "-c", "--config", + default="config.yaml", + help="Путь к конфигурационному файлу (по умолчанию: config.yaml)" + ) + parser.add_argument( + "--host", + help="Хост для привязки сервера" + ) + parser.add_argument( + "--port", + type=int, + help="Порт для привязки сервера" + ) + parser.add_argument( + "--debug", + action="store_true", + help="Включить отладочный режим" + ) + + args = parser.parse_args() + + config_path = args.config + if not Path(config_path).exists(): + print(f"Конфигурационный файл {config_path} не найден") + print("Используется конфигурация по умолчанию") + config = Config() + else: + try: + config = Config.from_yaml(config_path) + except Exception as e: + print(f"Ошибка загрузки конфигурации: {e}") + sys.exit(1) + + if args.host: + config.server.host = args.host + if args.port: + config.server.port = args.port + if args.debug: + config.logging.level = "DEBUG" + + server = PyServeServer(config) + + try: + server.run() + except KeyboardInterrupt: + print("\nСервер остановлен пользователем") + except Exception as e: + print(f"Ошибка запуска сервера: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main()