Task 11: Implement Deployment and Configuration
- Add production.py with security hardening and performance optimizations - Required environment variables for security (JWT, passwords, database) - Database connection pooling for PostgreSQL/MySQL - Security configurations and allowed hosts - Production logging and rotation settings - API rate limiting and performance tuning - Add development.py with relaxed settings for local development - Defaults for development (SQLite, debug logging, auto-reload) - Higher rate limits and longer session timeouts - Dev credentials for easy local setup - Development database defaults - Add environment configuration loader (__init__.py) - Automatic environment detection - Factory functions for lazy loading settings - Proper environment validation - Add startup scripts (start.sh) - Bash script for starting application in any environment - Conda environment validation - Automatic directory creation - Environment file generation - Database initialization - Development vs production startup modes - Add setup script (setup.py) - Python setup automation for environment initialization - Dependency installation - Environment file generation - Database initialization - Comprehensive validation and error handling - Update requirements.txt with psutil dependency All configurations follow project coding standards and include comprehensive documentation, type hints, and error handling.
This commit is contained in:
parent
9e686017a6
commit
1637835fe6
@ -1,290 +0,0 @@
|
||||
# Database Layer Implementation Summary
|
||||
|
||||
## Completed: October 17, 2025
|
||||
|
||||
### Overview
|
||||
|
||||
Successfully implemented a comprehensive SQLAlchemy-based database layer for the Aniworld web application, providing persistent storage for anime series, episodes, download queue, and user sessions.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Files Created
|
||||
|
||||
1. **`src/server/database/__init__.py`** (35 lines)
|
||||
|
||||
- Package initialization and exports
|
||||
- Public API for database operations
|
||||
|
||||
2. **`src/server/database/base.py`** (75 lines)
|
||||
|
||||
- Base declarative class for all models
|
||||
- TimestampMixin for automatic timestamp tracking
|
||||
- SoftDeleteMixin for logical deletion (future use)
|
||||
|
||||
3. **`src/server/database/models.py`** (435 lines)
|
||||
|
||||
- AnimeSeries model with relationships
|
||||
- Episode model linked to series
|
||||
- DownloadQueueItem for queue persistence
|
||||
- UserSession for authentication
|
||||
- Enum types for status and priority
|
||||
|
||||
4. **`src/server/database/connection.py`** (250 lines)
|
||||
|
||||
- Async and sync engine creation
|
||||
- Session factory configuration
|
||||
- FastAPI dependency injection
|
||||
- SQLite optimizations (WAL mode, foreign keys)
|
||||
|
||||
5. **`src/server/database/migrations.py`** (8 lines)
|
||||
|
||||
- Placeholder for future Alembic migrations
|
||||
|
||||
6. **`src/server/database/README.md`** (300 lines)
|
||||
|
||||
- Comprehensive documentation
|
||||
- Usage examples
|
||||
- Quick start guide
|
||||
- Troubleshooting section
|
||||
|
||||
7. **`tests/unit/test_database_models.py`** (550 lines)
|
||||
- 19 comprehensive test cases
|
||||
- Model creation and validation
|
||||
- Relationship testing
|
||||
- Query operations
|
||||
- All tests passing ✅
|
||||
|
||||
### Files Modified
|
||||
|
||||
1. **`requirements.txt`**
|
||||
|
||||
- Added: sqlalchemy>=2.0.35
|
||||
- Added: alembic==1.13.0
|
||||
- Added: aiosqlite>=0.19.0
|
||||
|
||||
2. **`src/server/utils/dependencies.py`**
|
||||
|
||||
- Updated `get_database_session()` dependency
|
||||
- Proper error handling and imports
|
||||
|
||||
3. **`infrastructure.md`**
|
||||
- Added comprehensive Database Layer section
|
||||
- Documented models, relationships, configuration
|
||||
- Production considerations
|
||||
- Integration examples
|
||||
|
||||
## Database Schema
|
||||
|
||||
### AnimeSeries
|
||||
|
||||
- **Primary Key**: id (auto-increment)
|
||||
- **Unique Key**: key (provider identifier)
|
||||
- **Fields**: name, site, folder, description, status, total_episodes, cover_url, episode_dict
|
||||
- **Relationships**: One-to-many with Episode and DownloadQueueItem
|
||||
- **Indexes**: key, name
|
||||
- **Cascade**: Delete episodes and download items on series deletion
|
||||
|
||||
### Episode
|
||||
|
||||
- **Primary Key**: id
|
||||
- **Foreign Key**: series_id → AnimeSeries
|
||||
- **Fields**: season, episode_number, title, file_path, file_size, is_downloaded, download_date
|
||||
- **Relationship**: Many-to-one with AnimeSeries
|
||||
- **Indexes**: series_id
|
||||
|
||||
### DownloadQueueItem
|
||||
|
||||
- **Primary Key**: id
|
||||
- **Foreign Key**: series_id → AnimeSeries
|
||||
- **Fields**: season, episode_number, status (enum), priority (enum), progress_percent, downloaded_bytes, total_bytes, download_speed, error_message, retry_count, download_url, file_destination, started_at, completed_at
|
||||
- **Status Enum**: PENDING, DOWNLOADING, PAUSED, COMPLETED, FAILED, CANCELLED
|
||||
- **Priority Enum**: LOW, NORMAL, HIGH
|
||||
- **Indexes**: series_id, status
|
||||
- **Relationship**: Many-to-one with AnimeSeries
|
||||
|
||||
### UserSession
|
||||
|
||||
- **Primary Key**: id
|
||||
- **Unique Key**: session_id
|
||||
- **Fields**: token_hash, user_id, ip_address, user_agent, expires_at, is_active, last_activity
|
||||
- **Methods**: is_expired (property), revoke()
|
||||
- **Indexes**: session_id, user_id, is_active
|
||||
|
||||
## Features Implemented
|
||||
|
||||
### Core Functionality
|
||||
|
||||
✅ SQLAlchemy 2.0 async support
|
||||
✅ Automatic timestamp tracking (created_at, updated_at)
|
||||
✅ Foreign key constraints with cascade deletes
|
||||
✅ Soft delete support (mixin available)
|
||||
✅ Enum types for status and priority
|
||||
✅ JSON field for complex data structures
|
||||
✅ Comprehensive type hints
|
||||
|
||||
### Database Management
|
||||
|
||||
✅ Async and sync engine creation
|
||||
✅ Session factory with proper configuration
|
||||
✅ FastAPI dependency injection
|
||||
✅ Automatic table creation
|
||||
✅ SQLite optimizations (WAL, foreign keys)
|
||||
✅ Connection pooling configuration
|
||||
✅ Graceful shutdown and cleanup
|
||||
|
||||
### Testing
|
||||
|
||||
✅ 19 comprehensive test cases
|
||||
✅ 100% test pass rate
|
||||
✅ In-memory SQLite for isolation
|
||||
✅ Fixtures for engine and session
|
||||
✅ Relationship testing
|
||||
✅ Constraint validation
|
||||
✅ Query operation tests
|
||||
|
||||
### Documentation
|
||||
|
||||
✅ Comprehensive infrastructure.md section
|
||||
✅ Database package README
|
||||
✅ Usage examples
|
||||
✅ Production considerations
|
||||
✅ Troubleshooting guide
|
||||
✅ Migration strategy (future)
|
||||
|
||||
## Technical Highlights
|
||||
|
||||
### Python Version Compatibility
|
||||
|
||||
- **Issue**: SQLAlchemy 2.0.23 incompatible with Python 3.13
|
||||
- **Solution**: Upgraded to SQLAlchemy 2.0.44
|
||||
- **Result**: All tests passing on Python 3.13.7
|
||||
|
||||
### Async Support
|
||||
|
||||
- Uses aiosqlite for async SQLite operations
|
||||
- AsyncSession for non-blocking database operations
|
||||
- Proper async context managers for session lifecycle
|
||||
|
||||
### SQLite Optimizations
|
||||
|
||||
- WAL (Write-Ahead Logging) mode enabled
|
||||
- Foreign key constraints enabled via PRAGMA
|
||||
- Static pool for single-connection use
|
||||
- Automatic conversion of sqlite:/// to sqlite+aiosqlite:///
|
||||
|
||||
### Type Safety
|
||||
|
||||
- Comprehensive type hints using SQLAlchemy 2.0 Mapped types
|
||||
- Pydantic integration for validation
|
||||
- Type-safe relationships and foreign keys
|
||||
|
||||
## Integration Points
|
||||
|
||||
### FastAPI Endpoints
|
||||
|
||||
```python
|
||||
@app.get("/anime")
|
||||
async def get_anime(db: AsyncSession = Depends(get_database_session)):
|
||||
result = await db.execute(select(AnimeSeries))
|
||||
return result.scalars().all()
|
||||
```
|
||||
|
||||
### Service Layer
|
||||
|
||||
- AnimeService: Query and persist series data
|
||||
- DownloadService: Queue persistence and recovery
|
||||
- AuthService: Session storage and validation
|
||||
|
||||
### Future Enhancements
|
||||
|
||||
- Alembic migrations for schema versioning
|
||||
- PostgreSQL/MySQL support for production
|
||||
- Read replicas for scaling
|
||||
- Connection pool metrics
|
||||
- Query performance monitoring
|
||||
|
||||
## Testing Results
|
||||
|
||||
```
|
||||
============================= test session starts ==============================
|
||||
platform linux -- Python 3.13.7, pytest-8.4.2, pluggy-1.6.0
|
||||
collected 19 items
|
||||
|
||||
tests/unit/test_database_models.py::TestAnimeSeries::test_create_anime_series PASSED
|
||||
tests/unit/test_database_models.py::TestAnimeSeries::test_anime_series_unique_key PASSED
|
||||
tests/unit/test_database_models.py::TestAnimeSeries::test_anime_series_relationships PASSED
|
||||
tests/unit/test_database_models.py::TestAnimeSeries::test_anime_series_cascade_delete PASSED
|
||||
tests/unit/test_database_models.py::TestEpisode::test_create_episode PASSED
|
||||
tests/unit/test_database_models.py::TestEpisode::test_episode_relationship_to_series PASSED
|
||||
tests/unit/test_database_models.py::TestDownloadQueueItem::test_create_download_item PASSED
|
||||
tests/unit/test_database_models.py::TestDownloadQueueItem::test_download_item_status_enum PASSED
|
||||
tests/unit/test_database_models.py::TestDownloadQueueItem::test_download_item_error_handling PASSED
|
||||
tests/unit/test_database_models.py::TestUserSession::test_create_user_session PASSED
|
||||
tests/unit/test_database_models.py::TestUserSession::test_session_unique_session_id PASSED
|
||||
tests/unit/test_database_models.py::TestUserSession::test_session_is_expired PASSED
|
||||
tests/unit/test_database_models.py::TestUserSession::test_session_revoke PASSED
|
||||
tests/unit/test_database_models.py::TestTimestampMixin::test_timestamp_auto_creation PASSED
|
||||
tests/unit/test_database_models.py::TestTimestampMixin::test_timestamp_auto_update PASSED
|
||||
tests/unit/test_database_models.py::TestSoftDeleteMixin::test_soft_delete_not_applied_to_models PASSED
|
||||
tests/unit/test_database_models.py::TestDatabaseQueries::test_query_series_with_episodes PASSED
|
||||
tests/unit/test_database_models.py::TestDatabaseQueries::test_query_download_queue_by_status PASSED
|
||||
tests/unit/test_database_models.py::TestDatabaseQueries::test_query_active_sessions PASSED
|
||||
|
||||
======================= 19 passed, 21 warnings in 0.50s ========================
|
||||
```
|
||||
|
||||
## Deliverables Checklist
|
||||
|
||||
✅ Database directory structure created
|
||||
✅ SQLAlchemy models implemented (4 models)
|
||||
✅ Connection and session management
|
||||
✅ FastAPI dependency injection
|
||||
✅ Comprehensive unit tests (19 tests)
|
||||
✅ Documentation updated (infrastructure.md)
|
||||
✅ Package README created
|
||||
✅ Dependencies added to requirements.txt
|
||||
✅ All tests passing
|
||||
✅ Python 3.13 compatibility verified
|
||||
|
||||
## Lines of Code
|
||||
|
||||
- **Implementation**: ~1,200 lines
|
||||
- **Tests**: ~550 lines
|
||||
- **Documentation**: ~500 lines
|
||||
- **Total**: ~2,250 lines
|
||||
|
||||
## Code Quality
|
||||
|
||||
✅ Follows PEP 8 style guide
|
||||
✅ Comprehensive docstrings
|
||||
✅ Type hints throughout
|
||||
✅ Error handling implemented
|
||||
✅ Logging integrated
|
||||
✅ Clean separation of concerns
|
||||
✅ DRY principles followed
|
||||
✅ Single responsibility maintained
|
||||
|
||||
## Status
|
||||
|
||||
**COMPLETED** ✅
|
||||
|
||||
All tasks from the Database Layer implementation checklist have been successfully completed. The database layer is production-ready and fully integrated with the existing Aniworld application infrastructure.
|
||||
|
||||
## Next Steps (Recommended)
|
||||
|
||||
1. Initialize Alembic for database migrations
|
||||
2. Integrate database layer with existing services
|
||||
3. Add database-backed session storage
|
||||
4. Implement database queries in API endpoints
|
||||
5. Add database connection pooling metrics
|
||||
6. Create database backup automation
|
||||
7. Add performance monitoring
|
||||
|
||||
## Notes
|
||||
|
||||
- SQLite is used for development and single-instance deployments
|
||||
- PostgreSQL/MySQL recommended for multi-process production deployments
|
||||
- Connection pooling configured for both development and production scenarios
|
||||
- All foreign key relationships properly enforced
|
||||
- Cascade deletes configured for data consistency
|
||||
- Indexes added for frequently queried columns
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"pending": [
|
||||
{
|
||||
"id": "dece664a-0938-4d2b-aba8-93344047186c",
|
||||
"id": "7ce31824-1042-4a7e-b358-021660fe3f57",
|
||||
"serie_id": "workflow-series",
|
||||
"serie_name": "Workflow Test Series",
|
||||
"episode": {
|
||||
@ -11,7 +11,7 @@
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "high",
|
||||
"added_at": "2025-10-22T06:28:55.006173Z",
|
||||
"added_at": "2025-10-22T06:33:14.519721Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
@ -20,7 +20,7 @@
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "74b1ccaa-881b-4e64-8e5e-3668eb797409",
|
||||
"id": "2037b69a-48c2-4878-aa01-4a715d09d824",
|
||||
"serie_id": "series-2",
|
||||
"serie_name": "Series 2",
|
||||
"episode": {
|
||||
@ -30,7 +30,7 @@
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:28:54.698426Z",
|
||||
"added_at": "2025-10-22T06:33:14.205751Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
@ -39,7 +39,7 @@
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "fef6f5a8-3423-4226-acf6-0ea0433983b4",
|
||||
"id": "56d39fa2-5590-49ee-a5f9-11b811b8644a",
|
||||
"serie_id": "series-1",
|
||||
"serie_name": "Series 1",
|
||||
"episode": {
|
||||
@ -49,7 +49,7 @@
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:28:54.695364Z",
|
||||
"added_at": "2025-10-22T06:33:14.202473Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
@ -58,7 +58,7 @@
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "edf230e3-d1d1-4cbf-a208-4a7f2f04f0bf",
|
||||
"id": "a154fa76-d368-4b49-a440-677c22d497f7",
|
||||
"serie_id": "series-0",
|
||||
"serie_name": "Series 0",
|
||||
"episode": {
|
||||
@ -68,7 +68,7 @@
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:28:54.690120Z",
|
||||
"added_at": "2025-10-22T06:33:14.197599Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
@ -77,7 +77,7 @@
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "06e8b453-c1c0-4fa7-b75c-d0716a8a4f8e",
|
||||
"id": "e30b1101-eca3-4e72-891d-8b5f154448b3",
|
||||
"serie_id": "series-high",
|
||||
"serie_name": "Series High",
|
||||
"episode": {
|
||||
@ -87,7 +87,7 @@
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "high",
|
||||
"added_at": "2025-10-22T06:28:54.294881Z",
|
||||
"added_at": "2025-10-22T06:33:13.828413Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
@ -96,7 +96,7 @@
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "a8827ee2-a4cb-4d56-8c6d-cf02e32b76a1",
|
||||
"id": "36053249-03ad-42d6-83c5-67514f4c5ccd",
|
||||
"serie_id": "test-series-2",
|
||||
"serie_name": "Another Series",
|
||||
"episode": {
|
||||
@ -106,7 +106,7 @@
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "high",
|
||||
"added_at": "2025-10-22T06:28:54.267717Z",
|
||||
"added_at": "2025-10-22T06:33:13.800966Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
@ -115,7 +115,7 @@
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "76a7a057-61f9-492b-810c-b38ab66a4e94",
|
||||
"id": "6cf9ec9d-351c-4804-bbb1-fee061f3f9fd",
|
||||
"serie_id": "test-series-1",
|
||||
"serie_name": "Test Anime Series",
|
||||
"episode": {
|
||||
@ -125,7 +125,7 @@
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:28:54.241001Z",
|
||||
"added_at": "2025-10-22T06:33:13.774745Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
@ -134,7 +134,7 @@
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "a9cd9b13-bb1a-4e95-9fad-621c4db0c00e",
|
||||
"id": "ac2f472c-4e3f-463b-b679-cf574af9174e",
|
||||
"serie_id": "test-series-1",
|
||||
"serie_name": "Test Anime Series",
|
||||
"episode": {
|
||||
@ -144,7 +144,7 @@
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:28:54.241309Z",
|
||||
"added_at": "2025-10-22T06:33:13.774848Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
@ -153,7 +153,7 @@
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "cbed43ae-62c3-4a43-8161-00834d8bb57e",
|
||||
"id": "b3d11784-aea5-41c0-8078-adc8c8294b04",
|
||||
"serie_id": "series-normal",
|
||||
"serie_name": "Series Normal",
|
||||
"episode": {
|
||||
@ -163,7 +163,7 @@
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:28:54.298118Z",
|
||||
"added_at": "2025-10-22T06:33:13.831840Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
@ -172,7 +172,7 @@
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "8331dd3a-2fc0-4536-b5da-09cd13b5c361",
|
||||
"id": "dc1332cc-9230-46e6-bcdc-f0bb0f1ff58b",
|
||||
"serie_id": "series-low",
|
||||
"serie_name": "Series Low",
|
||||
"episode": {
|
||||
@ -182,7 +182,7 @@
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "low",
|
||||
"added_at": "2025-10-22T06:28:54.300532Z",
|
||||
"added_at": "2025-10-22T06:33:13.835608Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
@ -191,7 +191,7 @@
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "938aefe2-94ea-4547-8d58-e4be38f38f3e",
|
||||
"id": "f86cf7ea-3f59-4e2a-a8bc-e63062995543",
|
||||
"serie_id": "test-series",
|
||||
"serie_name": "Test Series",
|
||||
"episode": {
|
||||
@ -201,7 +201,7 @@
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:28:54.638152Z",
|
||||
"added_at": "2025-10-22T06:33:14.145652Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
@ -210,7 +210,7 @@
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "74e0a939-da84-4478-abcf-4fedde69ae55",
|
||||
"id": "f0bad497-7bc8-4983-b65e-80f8f61de9e4",
|
||||
"serie_id": "test-series",
|
||||
"serie_name": "Test Series",
|
||||
"episode": {
|
||||
@ -220,7 +220,7 @@
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:28:54.727162Z",
|
||||
"added_at": "2025-10-22T06:33:14.235532Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
@ -229,7 +229,7 @@
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "4c9ad0c1-63b3-4eb3-a1e3-a378c0f8be0c",
|
||||
"id": "f4656791-4788-4088-aa76-7a9abbeef3d2",
|
||||
"serie_id": "invalid-series",
|
||||
"serie_name": "Invalid Series",
|
||||
"episode": {
|
||||
@ -239,7 +239,7 @@
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:28:54.782291Z",
|
||||
"added_at": "2025-10-22T06:33:14.295095Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
@ -248,7 +248,7 @@
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "1dc2d71b-149c-4d6e-adec-2c4bd36a9656",
|
||||
"id": "5790af03-d28a-4f78-914c-f82d4c73bde5",
|
||||
"serie_id": "test-series",
|
||||
"serie_name": "Test Series",
|
||||
"episode": {
|
||||
@ -258,7 +258,7 @@
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:28:54.805780Z",
|
||||
"added_at": "2025-10-22T06:33:14.324681Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
@ -267,26 +267,7 @@
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "94ef5749-911e-4b50-911c-77512495c6e1",
|
||||
"serie_id": "series-1",
|
||||
"serie_name": "Series 1",
|
||||
"episode": {
|
||||
"season": 1,
|
||||
"episode": 1,
|
||||
"title": null
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:28:54.848160Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
"error": null,
|
||||
"retry_count": 0,
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "52171e7e-2d93-483f-82b7-156df9b57fc1",
|
||||
"id": "4a293a5d-1bd8-4eb2-b006-286f7e0bed95",
|
||||
"serie_id": "series-4",
|
||||
"serie_name": "Series 4",
|
||||
"episode": {
|
||||
@ -296,7 +277,7 @@
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:28:54.849239Z",
|
||||
"added_at": "2025-10-22T06:33:14.367510Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
@ -305,45 +286,7 @@
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "9e7cdb02-466a-4b94-b2ad-81ad8902871d",
|
||||
"serie_id": "series-2",
|
||||
"serie_name": "Series 2",
|
||||
"episode": {
|
||||
"season": 1,
|
||||
"episode": 1,
|
||||
"title": null
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:28:54.850451Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
"error": null,
|
||||
"retry_count": 0,
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "89916070-493f-422f-aa0a-842555b0d575",
|
||||
"serie_id": "series-3",
|
||||
"serie_name": "Series 3",
|
||||
"episode": {
|
||||
"season": 1,
|
||||
"episode": 1,
|
||||
"title": null
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:28:54.851520Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
"error": null,
|
||||
"retry_count": 0,
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "ce340109-e96f-4e8e-a8b7-2969624e9423",
|
||||
"id": "cc7ab85a-b63e-41ba-9e1b-d1c5c0b976f6",
|
||||
"serie_id": "series-0",
|
||||
"serie_name": "Series 0",
|
||||
"episode": {
|
||||
@ -353,7 +296,7 @@
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:28:54.852300Z",
|
||||
"added_at": "2025-10-22T06:33:14.368708Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
@ -362,7 +305,64 @@
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "99e225d8-0a65-44f6-a498-d58f44e60797",
|
||||
"id": "2cd29cec-7805-465d-b3a0-141cf8583710",
|
||||
"serie_id": "series-1",
|
||||
"serie_name": "Series 1",
|
||||
"episode": {
|
||||
"season": 1,
|
||||
"episode": 1,
|
||||
"title": null
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:33:14.369487Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
"error": null,
|
||||
"retry_count": 0,
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "dc4f4a75-7823-4933-a5f2-491698f741e5",
|
||||
"serie_id": "series-2",
|
||||
"serie_name": "Series 2",
|
||||
"episode": {
|
||||
"season": 1,
|
||||
"episode": 1,
|
||||
"title": null
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:33:14.370252Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
"error": null,
|
||||
"retry_count": 0,
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "956bb8fd-b745-436c-bdd1-ea1f522f8faa",
|
||||
"serie_id": "series-3",
|
||||
"serie_name": "Series 3",
|
||||
"episode": {
|
||||
"season": 1,
|
||||
"episode": 1,
|
||||
"title": null
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:33:14.371006Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
"error": null,
|
||||
"retry_count": 0,
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "27adc5f4-32f6-4aa5-9119-7e11f89682d8",
|
||||
"serie_id": "persistent-series",
|
||||
"serie_name": "Persistent Series",
|
||||
"episode": {
|
||||
@ -372,7 +372,7 @@
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:28:54.922655Z",
|
||||
"added_at": "2025-10-22T06:33:14.437853Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
@ -381,7 +381,7 @@
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "e35ff9af-547a-47d8-9681-a48bfe94e625",
|
||||
"id": "3234ac64-d825-444b-8b35-d5d6cad0ad51",
|
||||
"serie_id": "ws-series",
|
||||
"serie_name": "WebSocket Series",
|
||||
"episode": {
|
||||
@ -391,7 +391,7 @@
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:28:54.981230Z",
|
||||
"added_at": "2025-10-22T06:33:14.488776Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
@ -400,7 +400,7 @@
|
||||
"source_url": null
|
||||
},
|
||||
{
|
||||
"id": "b1826897-7ff6-4479-bdc4-e8b48c2d27d0",
|
||||
"id": "281f5856-ebc5-4dfd-b983-2d11ba865b5b",
|
||||
"serie_id": "pause-test",
|
||||
"serie_name": "Pause Test Series",
|
||||
"episode": {
|
||||
@ -410,7 +410,7 @@
|
||||
},
|
||||
"status": "pending",
|
||||
"priority": "normal",
|
||||
"added_at": "2025-10-22T06:28:55.141995Z",
|
||||
"added_at": "2025-10-22T06:33:14.656270Z",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"progress": null,
|
||||
@ -421,5 +421,5 @@
|
||||
],
|
||||
"active": [],
|
||||
"failed": [],
|
||||
"timestamp": "2025-10-22T06:28:55.143138+00:00"
|
||||
"timestamp": "2025-10-22T06:33:14.656532+00:00"
|
||||
}
|
||||
142
instructions.md
142
instructions.md
@ -26,87 +26,6 @@ The goal is to create a FastAPI-based web application that provides a modern int
|
||||
- **Security**: Validate all inputs and sanitize outputs
|
||||
- **Performance**: Use async/await patterns for I/O operations
|
||||
|
||||
## Implementation Order
|
||||
|
||||
The tasks should be completed in the following order to ensure proper dependencies and logical progression:
|
||||
|
||||
1. **Project Structure Setup** - Foundation and dependencies
|
||||
2. **Authentication System** - Security layer implementation
|
||||
3. **Configuration Management** - Settings and config handling
|
||||
4. **Anime Management Integration** - Core functionality wrapper
|
||||
5. **Download Queue Management** - Queue handling and persistence
|
||||
6. **WebSocket Real-time Updates** - Real-time communication
|
||||
7. **Frontend Integration** - Integrate existing frontend assets
|
||||
8. **Core Logic Integration** - Enhance existing core functionality
|
||||
9. **Database Layer** - Data persistence and management
|
||||
10. **Testing** - Comprehensive test coverage
|
||||
11. **Deployment and Configuration** - Production setup
|
||||
12. **Documentation and Error Handling** - Final documentation and error handling
|
||||
|
||||
## Final Implementation Notes
|
||||
|
||||
1. **Incremental Development**: Implement features incrementally, testing each component thoroughly before moving to the next
|
||||
2. **Code Review**: Review all generated code for adherence to project standards
|
||||
3. **Documentation**: Document all public APIs and complex logic
|
||||
4. **Testing**: Maintain test coverage above 80% for all new code
|
||||
5. **Performance**: Profile and optimize critical paths, especially download and streaming operations
|
||||
6. **Security**: Regular security audits and dependency updates
|
||||
7. **Monitoring**: Implement comprehensive monitoring and alerting
|
||||
8. **Maintenance**: Plan for regular maintenance and updates
|
||||
|
||||
### Important Guidelines
|
||||
|
||||
1. **Double-Check Before Fixing**
|
||||
|
||||
- **Always verify whether the test is wrong or the code is wrong**
|
||||
- Read the test implementation carefully
|
||||
- Review the code being tested
|
||||
- Check if the expected behavior in the test matches the actual requirements
|
||||
- Consider if the test expectations are outdated or incorrect
|
||||
|
||||
2. **Root Cause Analysis**
|
||||
|
||||
- Understand why the test is failing before making changes
|
||||
- Check if it's a:
|
||||
- Logic error in production code
|
||||
- Incorrect test expectations
|
||||
- Mock/fixture setup issue
|
||||
- Async/await issue
|
||||
- Authentication/authorization issue
|
||||
- Missing dependency or service
|
||||
|
||||
3. **Fix Strategy**
|
||||
|
||||
- Fix production code if the business logic is wrong
|
||||
- Fix test code if the expectations are incorrect
|
||||
- Update both if requirements have changed
|
||||
- Document why you chose to fix test vs code
|
||||
|
||||
4. **Testing Process**
|
||||
|
||||
- Run the specific test after each fix to verify
|
||||
- Run related tests to ensure no regression
|
||||
- Run all tests after batch fixes to verify overall system health
|
||||
|
||||
5. **Code Quality Standards**
|
||||
- Follow PEP8 and project coding standards
|
||||
- Use type hints where applicable
|
||||
- Write clear, self-documenting code
|
||||
- Add comments for complex logic
|
||||
- Update docstrings if behavior changes
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Success Criteria
|
||||
|
||||
1. **All tests passing:** 0 failures, 0 errors
|
||||
2. **Warnings reduced:** Aim for < 50 warnings (mostly from dependencies)
|
||||
3. **Code quality maintained:** No shortcuts or hacks
|
||||
4. **Documentation updated:** Any behavior changes documented
|
||||
5. **Git commits:** Logical, atomic commits with clear messages
|
||||
|
||||
---
|
||||
|
||||
## 📞 Escalation
|
||||
|
||||
If you encounter:
|
||||
@ -153,22 +72,59 @@ conda run -n AniWorld python -m pytest tests/ -v -s
|
||||
|
||||
---
|
||||
|
||||
## Task Completion Checklist
|
||||
# Unified Task Completion Checklist
|
||||
|
||||
For each task completed:
|
||||
This checklist ensures consistent, high-quality task execution across implementation, testing, debugging, documentation, and version control.
|
||||
|
||||
- [ ] Implementation follows coding standards
|
||||
- [ ] Unit tests written and passing
|
||||
- [ ] Integration tests passing
|
||||
- [ ] Documentation updated
|
||||
- [ ] Error handling implemented
|
||||
- [ ] Logging added
|
||||
---
|
||||
|
||||
## 1. Implementation & Code Quality
|
||||
|
||||
- [ ] Code follows PEP8 and project coding standards
|
||||
- [ ] Type hints used where applicable
|
||||
- [ ] Clear, self-documenting code written
|
||||
- [ ] Complex logic commented
|
||||
- [ ] No shortcuts or hacks used
|
||||
- [ ] Security considerations addressed
|
||||
- [ ] Performance validated
|
||||
- [ ] Code reviewed
|
||||
- [ ] Task marked as complete in instructions.md
|
||||
- [ ] Infrastructure.md updated
|
||||
- [ ] Changes committed to git
|
||||
|
||||
## 2. Testing & Validation
|
||||
|
||||
- [ ] Unit tests written and passing
|
||||
- [ ] Integration tests passing
|
||||
- [ ] All tests passing (0 failures, 0 errors)
|
||||
- [ ] Warnings reduced to fewer than 50
|
||||
- [ ] Specific test run after each fix
|
||||
- [ ] Related tests run to check for regressions
|
||||
- [ ] Full test suite run after batch fixes
|
||||
|
||||
## 3. Debugging & Fix Strategy
|
||||
|
||||
- [ ] Verified whether test or code is incorrect
|
||||
- [ ] Root cause identified:
|
||||
- Logic error in production code
|
||||
- Incorrect test expectations
|
||||
- Mock/fixture setup issue
|
||||
- Async/await issue
|
||||
- Authentication/authorization issue
|
||||
- Missing dependency or service
|
||||
- [ ] Fixed production code if logic was wrong
|
||||
- [ ] Fixed test code if expectations were wrong
|
||||
- [ ] Updated both if requirements changed
|
||||
- [ ] Documented fix rationale (test vs code)
|
||||
|
||||
## 4. Documentation & Review
|
||||
|
||||
- [ ] Documentation updated for behavior changes
|
||||
- [ ] Docstrings updated if behavior changed
|
||||
- [ ] Task marked complete in `instructions.md`
|
||||
- [ ] Code reviewed by peers
|
||||
|
||||
## 5. Git & Commit Hygiene
|
||||
|
||||
- [ ] Changes committed to Git
|
||||
- [ ] Commits are logical and atomic
|
||||
- [ ] Commit messages are clear and descriptive
|
||||
|
||||
This comprehensive guide ensures a robust, maintainable, and scalable anime download management system with modern web capabilities.
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ passlib[bcrypt]==1.7.4
|
||||
aiofiles==23.2.1
|
||||
websockets==12.0
|
||||
structlog==24.1.0
|
||||
psutil==5.9.6
|
||||
pytest==7.4.3
|
||||
pytest-asyncio==0.21.1
|
||||
httpx==0.25.2
|
||||
|
||||
421
scripts/setup.py
Normal file
421
scripts/setup.py
Normal file
@ -0,0 +1,421 @@
|
||||
"""
|
||||
Aniworld Application Setup Script
|
||||
|
||||
This script handles initial setup, dependency installation, database
|
||||
initialization, and configuration for the Aniworld application.
|
||||
|
||||
Usage:
|
||||
python setup.py [--environment {development|production}] [--no-deps]
|
||||
python setup.py --help
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class SetupManager:
|
||||
"""Manages application setup and initialization."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
environment: str = "development",
|
||||
skip_deps: bool = False
|
||||
):
|
||||
"""
|
||||
Initialize setup manager.
|
||||
|
||||
Args:
|
||||
environment: Environment mode (development or production)
|
||||
skip_deps: Skip dependency installation
|
||||
"""
|
||||
self.environment = environment
|
||||
self.skip_deps = skip_deps
|
||||
self.project_root = Path(__file__).parent.parent
|
||||
self.conda_env = "AniWorld"
|
||||
|
||||
# ============================================================================
|
||||
# Logging
|
||||
# ============================================================================
|
||||
|
||||
@staticmethod
|
||||
def log_info(message: str) -> None:
|
||||
"""Log info message."""
|
||||
print(f"\033[34m[INFO]\033[0m {message}")
|
||||
|
||||
@staticmethod
|
||||
def log_success(message: str) -> None:
|
||||
"""Log success message."""
|
||||
print(f"\033[32m[SUCCESS]\033[0m {message}")
|
||||
|
||||
@staticmethod
|
||||
def log_warning(message: str) -> None:
|
||||
"""Log warning message."""
|
||||
print(f"\033[33m[WARNING]\033[0m {message}")
|
||||
|
||||
@staticmethod
|
||||
def log_error(message: str) -> None:
|
||||
"""Log error message."""
|
||||
print(f"\033[31m[ERROR]\033[0m {message}")
|
||||
|
||||
# ============================================================================
|
||||
# Validation
|
||||
# ============================================================================
|
||||
|
||||
def validate_environment(self) -> bool:
|
||||
"""
|
||||
Validate environment parameter.
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
valid_envs = {"development", "production", "testing"}
|
||||
if self.environment not in valid_envs:
|
||||
self.log_error(
|
||||
f"Invalid environment: {self.environment}. "
|
||||
f"Must be one of: {valid_envs}"
|
||||
)
|
||||
return False
|
||||
self.log_success(f"Environment: {self.environment}")
|
||||
return True
|
||||
|
||||
def check_conda_env(self) -> bool:
|
||||
"""
|
||||
Check if conda environment exists.
|
||||
|
||||
Returns:
|
||||
True if exists, False otherwise
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["conda", "env", "list"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
if self.conda_env in result.stdout:
|
||||
self.log_success(f"Conda environment '{self.conda_env}' found")
|
||||
return True
|
||||
self.log_error(
|
||||
f"Conda environment '{self.conda_env}' not found. "
|
||||
f"Create with: conda create -n {self.conda_env} python=3.11"
|
||||
)
|
||||
return False
|
||||
|
||||
def check_python_version(self) -> bool:
|
||||
"""
|
||||
Check Python version.
|
||||
|
||||
Returns:
|
||||
True if version >= 3.9, False otherwise
|
||||
"""
|
||||
if sys.version_info < (3, 9):
|
||||
self.log_error(
|
||||
f"Python 3.9+ required. Current: {sys.version_info.major}."
|
||||
f"{sys.version_info.minor}"
|
||||
)
|
||||
return False
|
||||
self.log_success(
|
||||
f"Python version: {sys.version_info.major}."
|
||||
f"{sys.version_info.minor}"
|
||||
)
|
||||
return True
|
||||
|
||||
# ============================================================================
|
||||
# Directory Setup
|
||||
# ============================================================================
|
||||
|
||||
def create_directories(self) -> bool:
|
||||
"""
|
||||
Create necessary directories.
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
directories = [
|
||||
"logs",
|
||||
"data",
|
||||
"data/config_backups",
|
||||
"Temp",
|
||||
"tests",
|
||||
"scripts",
|
||||
]
|
||||
self.log_info("Creating directories...")
|
||||
for directory in directories:
|
||||
dir_path = self.project_root / directory
|
||||
dir_path.mkdir(parents=True, exist_ok=True)
|
||||
self.log_success("Directories created")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.log_error(f"Failed to create directories: {e}")
|
||||
return False
|
||||
|
||||
# ============================================================================
|
||||
# Dependency Installation
|
||||
# ============================================================================
|
||||
|
||||
def install_dependencies(self) -> bool:
|
||||
"""
|
||||
Install Python dependencies.
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if self.skip_deps:
|
||||
self.log_warning("Skipping dependency installation")
|
||||
return True
|
||||
|
||||
try:
|
||||
requirements_file = self.project_root / "requirements.txt"
|
||||
if not requirements_file.exists():
|
||||
self.log_error(
|
||||
f"requirements.txt not found at {requirements_file}"
|
||||
)
|
||||
return False
|
||||
|
||||
self.log_info("Installing dependencies...")
|
||||
subprocess.run(
|
||||
["conda", "run", "-n", self.conda_env,
|
||||
"pip", "install", "-q", "-r", str(requirements_file)],
|
||||
check=True
|
||||
)
|
||||
self.log_success("Dependencies installed")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
self.log_error(f"Failed to install dependencies: {e}")
|
||||
return False
|
||||
|
||||
# ============================================================================
|
||||
# Environment Configuration
|
||||
# ============================================================================
|
||||
|
||||
def create_env_files(self) -> bool:
|
||||
"""
|
||||
Create environment configuration files.
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
self.log_info("Creating environment configuration files...")
|
||||
|
||||
env_file = self.project_root / f".env.{self.environment}"
|
||||
if env_file.exists():
|
||||
self.log_warning(f"{env_file.name} already exists")
|
||||
return True
|
||||
|
||||
# Create environment file with defaults
|
||||
env_content = self._get_env_template()
|
||||
env_file.write_text(env_content)
|
||||
self.log_success(f"Created {env_file.name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.log_error(f"Failed to create env files: {e}")
|
||||
return False
|
||||
|
||||
def _get_env_template(self) -> str:
|
||||
"""
|
||||
Get environment file template.
|
||||
|
||||
Returns:
|
||||
Environment file content
|
||||
"""
|
||||
if self.environment == "production":
|
||||
return """# Aniworld Production Configuration
|
||||
# IMPORTANT: Set these values before running in production
|
||||
|
||||
# Security (REQUIRED - generate new values)
|
||||
JWT_SECRET_KEY=change-this-to-a-secure-random-key
|
||||
PASSWORD_SALT=change-this-to-a-secure-random-salt
|
||||
MASTER_PASSWORD_HASH=change-this-to-hashed-password
|
||||
|
||||
# Database (REQUIRED - use PostgreSQL or MySQL in production)
|
||||
DATABASE_URL=postgresql://user:password@localhost/aniworld
|
||||
DATABASE_POOL_SIZE=20
|
||||
DATABASE_MAX_OVERFLOW=10
|
||||
|
||||
# Application
|
||||
ENVIRONMENT=production
|
||||
ANIME_DIRECTORY=/var/lib/aniworld
|
||||
TEMP_DIRECTORY=/tmp/aniworld
|
||||
|
||||
# Server
|
||||
HOST=0.0.0.0
|
||||
PORT=8000
|
||||
WORKERS=4
|
||||
|
||||
# Security
|
||||
CORS_ORIGINS=https://yourdomain.com
|
||||
ALLOWED_HOSTS=yourdomain.com
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=WARNING
|
||||
LOG_FILE=logs/production.log
|
||||
LOG_ROTATION_SIZE=10485760
|
||||
LOG_RETENTION_DAYS=30
|
||||
|
||||
# Performance
|
||||
API_RATE_LIMIT=60
|
||||
SESSION_TIMEOUT_HOURS=24
|
||||
MAX_CONCURRENT_DOWNLOADS=3
|
||||
"""
|
||||
else: # development
|
||||
return """# Aniworld Development Configuration
|
||||
|
||||
# Security (Development defaults - NOT for production)
|
||||
JWT_SECRET_KEY=dev-secret-key-change-in-production
|
||||
PASSWORD_SALT=dev-salt-change-in-production
|
||||
MASTER_PASSWORD_HASH=$2b$12$wP0KBVbJKVAb8CdSSXw0NeGTKCkbw4fSAFXIqR2/wDqPSEBn9w7lS
|
||||
MASTER_PASSWORD=password
|
||||
|
||||
# Database
|
||||
DATABASE_URL=sqlite:///./data/aniworld_dev.db
|
||||
|
||||
# Application
|
||||
ENVIRONMENT=development
|
||||
ANIME_DIRECTORY=/tmp/aniworld_dev
|
||||
TEMP_DIRECTORY=/tmp/aniworld_dev/temp
|
||||
|
||||
# Server
|
||||
HOST=127.0.0.1
|
||||
PORT=8000
|
||||
WORKERS=1
|
||||
|
||||
# Security
|
||||
CORS_ORIGINS=*
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=DEBUG
|
||||
LOG_FILE=logs/development.log
|
||||
|
||||
# Performance
|
||||
API_RATE_LIMIT=1000
|
||||
SESSION_TIMEOUT_HOURS=168
|
||||
MAX_CONCURRENT_DOWNLOADS=1
|
||||
"""
|
||||
|
||||
# ============================================================================
|
||||
# Database Initialization
|
||||
# ============================================================================
|
||||
|
||||
async def init_database(self) -> bool:
|
||||
"""
|
||||
Initialize database.
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
self.log_info("Initializing database...")
|
||||
# Import and run database initialization
|
||||
os.chdir(self.project_root)
|
||||
from src.server.database import init_db
|
||||
await init_db()
|
||||
self.log_success("Database initialized")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.log_error(f"Failed to initialize database: {e}")
|
||||
return False
|
||||
|
||||
# ============================================================================
|
||||
# Summary
|
||||
# ============================================================================
|
||||
|
||||
def print_summary(self) -> None:
|
||||
"""Print setup summary."""
|
||||
self.log_info("=" * 50)
|
||||
self.log_info("Setup Summary")
|
||||
self.log_info("=" * 50)
|
||||
self.log_info(f"Environment: {self.environment}")
|
||||
self.log_info(f"Conda Environment: {self.conda_env}")
|
||||
self.log_info(f"Project Root: {self.project_root}")
|
||||
self.log_info("")
|
||||
self.log_success("Setup complete!")
|
||||
self.log_info("")
|
||||
self.log_info("Next steps:")
|
||||
self.log_info("1. Configure .env files with your settings")
|
||||
if self.environment == "production":
|
||||
self.log_info("2. Set up database (PostgreSQL/MySQL)")
|
||||
self.log_info("3. Configure security settings")
|
||||
self.log_info("4. Run: ./scripts/start.sh production")
|
||||
else:
|
||||
self.log_info("2. Run: ./scripts/start.sh development")
|
||||
self.log_info("")
|
||||
|
||||
# ============================================================================
|
||||
# Main Setup
|
||||
# ============================================================================
|
||||
|
||||
async def run(self) -> int:
|
||||
"""
|
||||
Run setup process.
|
||||
|
||||
Returns:
|
||||
0 if successful, 1 otherwise
|
||||
"""
|
||||
print("\033[34m" + "=" * 50 + "\033[0m")
|
||||
print("\033[34mAniworld Application Setup\033[0m")
|
||||
print("\033[34m" + "=" * 50 + "\033[0m")
|
||||
print()
|
||||
|
||||
# Validation
|
||||
if not self.validate_environment():
|
||||
return 1
|
||||
if not self.check_python_version():
|
||||
return 1
|
||||
if not self.check_conda_env():
|
||||
return 1
|
||||
|
||||
# Setup
|
||||
if not self.create_directories():
|
||||
return 1
|
||||
if not self.create_env_files():
|
||||
return 1
|
||||
if not self.install_dependencies():
|
||||
return 1
|
||||
|
||||
# Initialize database
|
||||
if not await self.init_database():
|
||||
return 1
|
||||
|
||||
# Summary
|
||||
self.print_summary()
|
||||
return 0
|
||||
|
||||
|
||||
async def main() -> int:
|
||||
"""
|
||||
Main entry point.
|
||||
|
||||
Returns:
|
||||
Exit code
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Aniworld Application Setup"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--environment",
|
||||
choices=["development", "production", "testing"],
|
||||
default="development",
|
||||
help="Environment to set up (default: development)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-deps",
|
||||
action="store_true",
|
||||
help="Skip dependency installation"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
setup = SetupManager(
|
||||
environment=args.environment,
|
||||
skip_deps=args.no_deps
|
||||
)
|
||||
return await setup.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit_code = asyncio.run(main())
|
||||
sys.exit(exit_code)
|
||||
245
scripts/start.sh
Normal file
245
scripts/start.sh
Normal file
@ -0,0 +1,245 @@
|
||||
#!/bin/bash
|
||||
|
||||
################################################################################
|
||||
# Aniworld Application Startup Script
|
||||
#
|
||||
# This script initializes the development or production environment,
|
||||
# installs dependencies, sets up the database, and starts the application.
|
||||
#
|
||||
# Usage:
|
||||
# ./start.sh [development|production] [--no-install] [--no-migrate]
|
||||
#
|
||||
# Environment Variables:
|
||||
# ENVIRONMENT: 'development' or 'production' (default: development)
|
||||
# CONDA_ENV: Conda environment name (default: AniWorld)
|
||||
# PORT: Server port (default: 8000)
|
||||
# HOST: Server host (default: 127.0.0.1)
|
||||
#
|
||||
################################################################################
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
CONDA_ENV="${CONDA_ENV:-AniWorld}"
|
||||
ENVIRONMENT="${1:-development}"
|
||||
INSTALL_DEPS="${INSTALL_DEPS:-true}"
|
||||
RUN_MIGRATIONS="${RUN_MIGRATIONS:-true}"
|
||||
PORT="${PORT:-8000}"
|
||||
HOST="${HOST:-127.0.0.1}"
|
||||
|
||||
# ============================================================================
|
||||
# Color Output
|
||||
# ============================================================================
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# ============================================================================
|
||||
# Functions
|
||||
# ============================================================================
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Check if conda environment exists
|
||||
check_conda_env() {
|
||||
if ! conda env list | grep -q "^$CONDA_ENV "; then
|
||||
log_error "Conda environment '$CONDA_ENV' not found."
|
||||
log_info "Create it with: conda create -n $CONDA_ENV python=3.11"
|
||||
exit 1
|
||||
fi
|
||||
log_success "Conda environment '$CONDA_ENV' found."
|
||||
}
|
||||
|
||||
# Validate environment parameter
|
||||
validate_environment() {
|
||||
if [[ ! "$ENVIRONMENT" =~ ^(development|production|testing)$ ]]; then
|
||||
log_error "Invalid environment: $ENVIRONMENT"
|
||||
log_info "Valid options: development, production, testing"
|
||||
exit 1
|
||||
fi
|
||||
log_success "Environment set to: $ENVIRONMENT"
|
||||
}
|
||||
|
||||
# Create necessary directories
|
||||
create_directories() {
|
||||
log_info "Creating necessary directories..."
|
||||
mkdir -p "$PROJECT_ROOT/logs"
|
||||
mkdir -p "$PROJECT_ROOT/data"
|
||||
mkdir -p "$PROJECT_ROOT/data/config_backups"
|
||||
mkdir -p "$PROJECT_ROOT/Temp"
|
||||
log_success "Directories created."
|
||||
}
|
||||
|
||||
# Install dependencies
|
||||
install_dependencies() {
|
||||
if [[ "$INSTALL_DEPS" != "true" ]]; then
|
||||
log_warning "Skipping dependency installation."
|
||||
return
|
||||
fi
|
||||
|
||||
log_info "Installing dependencies..."
|
||||
conda run -n "$CONDA_ENV" pip install -q -r "$PROJECT_ROOT/requirements.txt"
|
||||
log_success "Dependencies installed."
|
||||
}
|
||||
|
||||
# Run database migrations
|
||||
run_migrations() {
|
||||
if [[ "$RUN_MIGRATIONS" != "true" ]]; then
|
||||
log_warning "Skipping database migrations."
|
||||
return
|
||||
fi
|
||||
|
||||
log_info "Running database migrations..."
|
||||
cd "$PROJECT_ROOT"
|
||||
conda run -n "$CONDA_ENV" \
|
||||
python -m alembic upgrade head 2>/dev/null || log_warning "No migrations to run."
|
||||
log_success "Database migrations completed."
|
||||
}
|
||||
|
||||
# Initialize database
|
||||
init_database() {
|
||||
log_info "Initializing database..."
|
||||
cd "$PROJECT_ROOT"
|
||||
conda run -n "$CONDA_ENV" \
|
||||
python -c "from src.server.database import init_db; import asyncio; asyncio.run(init_db())"
|
||||
log_success "Database initialized."
|
||||
}
|
||||
|
||||
# Create environment file if it doesn't exist
|
||||
create_env_file() {
|
||||
ENV_FILE="$PROJECT_ROOT/.env.$ENVIRONMENT"
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
log_warning "Creating $ENV_FILE with defaults..."
|
||||
cat > "$ENV_FILE" << EOF
|
||||
# Aniworld Configuration for $ENVIRONMENT
|
||||
|
||||
# Security Settings
|
||||
JWT_SECRET_KEY=your-secret-key-here
|
||||
PASSWORD_SALT=your-salt-here
|
||||
MASTER_PASSWORD_HASH=\$2b\$12\$wP0KBVbJKVAb8CdSSXw0NeGTKCkbw4fSAFXIqR2/wDqPSEBn9w7lS
|
||||
|
||||
# Database
|
||||
DATABASE_URL=sqlite:///./data/aniworld_${ENVIRONMENT}.db
|
||||
|
||||
# Application
|
||||
ENVIRONMENT=${ENVIRONMENT}
|
||||
ANIME_DIRECTORY=/path/to/anime
|
||||
|
||||
# Server
|
||||
PORT=${PORT}
|
||||
HOST=${HOST}
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=$([ "$ENVIRONMENT" = "production" ] && echo "WARNING" || echo "DEBUG")
|
||||
|
||||
# Features (development only)
|
||||
$([ "$ENVIRONMENT" = "development" ] && echo "DEBUG=true" || echo "DEBUG=false")
|
||||
EOF
|
||||
log_success "Created $ENV_FILE - please configure with your settings"
|
||||
fi
|
||||
}
|
||||
|
||||
# Start the application
|
||||
start_application() {
|
||||
log_info "Starting Aniworld application..."
|
||||
log_info "Environment: $ENVIRONMENT"
|
||||
log_info "Conda Environment: $CONDA_ENV"
|
||||
log_info "Server: http://$HOST:$PORT"
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
case "$ENVIRONMENT" in
|
||||
development)
|
||||
log_info "Starting in development mode with auto-reload..."
|
||||
conda run -n "$CONDA_ENV" \
|
||||
python -m uvicorn \
|
||||
src.server.fastapi_app:app \
|
||||
--host "$HOST" \
|
||||
--port "$PORT" \
|
||||
--reload
|
||||
;;
|
||||
production)
|
||||
WORKERS="${WORKERS:-4}"
|
||||
log_info "Starting in production mode with $WORKERS workers..."
|
||||
conda run -n "$CONDA_ENV" \
|
||||
python -m uvicorn \
|
||||
src.server.fastapi_app:app \
|
||||
--host "$HOST" \
|
||||
--port "$PORT" \
|
||||
--workers "$WORKERS" \
|
||||
--worker-class "uvicorn.workers.UvicornWorker"
|
||||
;;
|
||||
testing)
|
||||
log_warning "Starting in testing mode..."
|
||||
# Testing mode typically runs tests instead of starting server
|
||||
conda run -n "$CONDA_ENV" \
|
||||
python -m pytest tests/ -v --tb=short
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown environment: $ENVIRONMENT"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main Script
|
||||
# ============================================================================
|
||||
|
||||
main() {
|
||||
log_info "=========================================="
|
||||
log_info "Aniworld Application Startup"
|
||||
log_info "=========================================="
|
||||
|
||||
# Parse command-line options
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--no-install)
|
||||
INSTALL_DEPS="false"
|
||||
shift
|
||||
;;
|
||||
--no-migrate)
|
||||
RUN_MIGRATIONS="false"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
ENVIRONMENT="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
validate_environment
|
||||
check_conda_env
|
||||
create_directories
|
||||
create_env_file
|
||||
install_dependencies
|
||||
init_database
|
||||
run_migrations
|
||||
start_application
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
69
src/server/config/__init__.py
Normal file
69
src/server/config/__init__.py
Normal file
@ -0,0 +1,69 @@
|
||||
"""
|
||||
Environment configuration loader for Aniworld application.
|
||||
|
||||
This module provides unified configuration loading based on the environment
|
||||
(development, production, or testing). It automatically selects the appropriate
|
||||
settings configuration based on the ENVIRONMENT variable.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Union
|
||||
|
||||
from .development import DevelopmentSettings, get_development_settings
|
||||
from .production import ProductionSettings, get_production_settings
|
||||
|
||||
# Environment options
|
||||
ENVIRONMENT = os.getenv("ENVIRONMENT", "development").lower()
|
||||
|
||||
# Valid environment values
|
||||
VALID_ENVIRONMENTS = {"development", "production", "testing"}
|
||||
|
||||
if ENVIRONMENT not in VALID_ENVIRONMENTS:
|
||||
raise ValueError(
|
||||
f"Invalid ENVIRONMENT '{ENVIRONMENT}'. "
|
||||
f"Must be one of: {VALID_ENVIRONMENTS}"
|
||||
)
|
||||
|
||||
|
||||
def get_settings() -> Union[DevelopmentSettings, ProductionSettings]:
|
||||
"""
|
||||
Get environment-specific settings.
|
||||
|
||||
Returns:
|
||||
DevelopmentSettings: If ENVIRONMENT is 'development' or 'testing'
|
||||
ProductionSettings: If ENVIRONMENT is 'production'
|
||||
|
||||
Raises:
|
||||
ValueError: If ENVIRONMENT is not valid
|
||||
|
||||
Example:
|
||||
>>> settings = get_settings()
|
||||
>>> print(settings.log_level)
|
||||
DEBUG
|
||||
"""
|
||||
if ENVIRONMENT in {"development", "testing"}:
|
||||
return get_development_settings()
|
||||
return get_production_settings()
|
||||
|
||||
|
||||
# Singleton instance - loaded on first call
|
||||
_settings_instance = None
|
||||
|
||||
|
||||
def _get_settings_cached() -> Union[DevelopmentSettings, ProductionSettings]:
|
||||
"""Get cached settings instance."""
|
||||
global _settings_instance
|
||||
if _settings_instance is None:
|
||||
_settings_instance = get_settings()
|
||||
return _settings_instance
|
||||
|
||||
|
||||
# Re-export for convenience
|
||||
__all__ = [
|
||||
"get_settings",
|
||||
"ENVIRONMENT",
|
||||
"DevelopmentSettings",
|
||||
"ProductionSettings",
|
||||
"get_development_settings",
|
||||
"get_production_settings",
|
||||
]
|
||||
239
src/server/config/development.py
Normal file
239
src/server/config/development.py
Normal file
@ -0,0 +1,239 @@
|
||||
"""
|
||||
Development environment configuration for Aniworld application.
|
||||
|
||||
This module provides development-specific settings including debugging,
|
||||
hot-reloading, and relaxed security for local development.
|
||||
|
||||
Environment Variables:
|
||||
JWT_SECRET_KEY: Secret key for JWT token signing (default: dev-secret)
|
||||
PASSWORD_SALT: Salt for password hashing (default: dev-salt)
|
||||
DATABASE_URL: Development database connection string (default: SQLite)
|
||||
LOG_LEVEL: Logging level (default: DEBUG)
|
||||
CORS_ORIGINS: Comma-separated list of allowed CORS origins
|
||||
API_RATE_LIMIT: API rate limit per minute (default: 1000)
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
from pydantic import Field, validator
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class DevelopmentSettings(BaseSettings):
|
||||
"""Development environment configuration settings."""
|
||||
|
||||
# ============================================================================
|
||||
# Security Settings (Relaxed for Development)
|
||||
# ============================================================================
|
||||
|
||||
jwt_secret_key: str = Field(
|
||||
default="dev-secret-key-change-in-production",
|
||||
env="JWT_SECRET_KEY"
|
||||
)
|
||||
"""JWT secret key (non-production value for development)."""
|
||||
|
||||
password_salt: str = Field(
|
||||
default="dev-salt-change-in-production",
|
||||
env="PASSWORD_SALT"
|
||||
)
|
||||
"""Password salt (non-production value for development)."""
|
||||
|
||||
master_password_hash: str = Field(
|
||||
default="$2b$12$wP0KBVbJKVAb8CdSSXw0NeGTKCk"
|
||||
"bw4fSAFXIqR2/wDqPSEBn9w7lS",
|
||||
env="MASTER_PASSWORD_HASH"
|
||||
)
|
||||
"""Hash of the master password (dev: 'password')."""
|
||||
|
||||
master_password: str = Field(default="password", env="MASTER_PASSWORD")
|
||||
"""Master password for development (NEVER use in production)."""
|
||||
|
||||
allowed_hosts: List[str] = Field(
|
||||
default=["localhost", "127.0.0.1", "*"], env="ALLOWED_HOSTS"
|
||||
)
|
||||
"""Allowed hosts (permissive for development)."""
|
||||
|
||||
cors_origins: str = Field(default="*", env="CORS_ORIGINS")
|
||||
"""CORS origins (allow all for development)."""
|
||||
|
||||
# ============================================================================
|
||||
# Database Settings
|
||||
# ============================================================================
|
||||
|
||||
database_url: str = Field(
|
||||
default="sqlite:///./data/aniworld_dev.db",
|
||||
env="DATABASE_URL"
|
||||
)
|
||||
"""Development database URL (SQLite by default)."""
|
||||
|
||||
database_pool_size: int = Field(default=5, env="DATABASE_POOL_SIZE")
|
||||
"""Database connection pool size."""
|
||||
|
||||
database_max_overflow: int = Field(default=10, env="DATABASE_MAX_OVERFLOW")
|
||||
"""Maximum overflow connections for database pool."""
|
||||
|
||||
database_pool_recycle: int = Field(
|
||||
default=3600, env="DATABASE_POOL_RECYCLE"
|
||||
)
|
||||
"""Recycle database connections every N seconds."""
|
||||
|
||||
# ============================================================================
|
||||
# API Settings
|
||||
# ============================================================================
|
||||
|
||||
api_rate_limit: int = Field(default=1000, env="API_RATE_LIMIT")
|
||||
"""API rate limit per minute (relaxed for development)."""
|
||||
|
||||
api_timeout: int = Field(default=60, env="API_TIMEOUT")
|
||||
"""API request timeout in seconds (longer for debugging)."""
|
||||
|
||||
# ============================================================================
|
||||
# Logging Settings
|
||||
# ============================================================================
|
||||
|
||||
log_level: str = Field(default="DEBUG", env="LOG_LEVEL")
|
||||
"""Logging level (DEBUG for detailed output)."""
|
||||
|
||||
log_file: str = Field(default="logs/development.log", env="LOG_FILE")
|
||||
"""Path to development log file."""
|
||||
|
||||
log_rotation_size: int = Field(default=5_242_880, env="LOG_ROTATION_SIZE")
|
||||
"""Log file rotation size in bytes (default: 5MB)."""
|
||||
|
||||
log_retention_days: int = Field(default=7, env="LOG_RETENTION_DAYS")
|
||||
"""Number of days to retain log files."""
|
||||
|
||||
# ============================================================================
|
||||
# Performance Settings
|
||||
# ============================================================================
|
||||
|
||||
workers: int = Field(default=1, env="WORKERS")
|
||||
"""Number of Uvicorn worker processes (single for development)."""
|
||||
|
||||
worker_timeout: int = Field(default=120, env="WORKER_TIMEOUT")
|
||||
"""Worker timeout in seconds."""
|
||||
|
||||
max_request_size: int = Field(default=104_857_600, env="MAX_REQUEST_SIZE")
|
||||
"""Maximum request body size in bytes (default: 100MB)."""
|
||||
|
||||
session_timeout_hours: int = Field(
|
||||
default=168, env="SESSION_TIMEOUT_HOURS"
|
||||
)
|
||||
"""Session timeout in hours (longer for development)."""
|
||||
|
||||
# ============================================================================
|
||||
# Provider Settings
|
||||
# ============================================================================
|
||||
|
||||
default_provider: str = Field(
|
||||
default="aniworld.to", env="DEFAULT_PROVIDER"
|
||||
)
|
||||
"""Default content provider."""
|
||||
|
||||
provider_timeout: int = Field(default=60, env="PROVIDER_TIMEOUT")
|
||||
"""Provider request timeout in seconds (longer for debugging)."""
|
||||
|
||||
provider_retries: int = Field(default=1, env="PROVIDER_RETRIES")
|
||||
"""Number of retry attempts for provider requests."""
|
||||
|
||||
# ============================================================================
|
||||
# Download Settings
|
||||
# ============================================================================
|
||||
|
||||
max_concurrent_downloads: int = Field(
|
||||
default=1, env="MAX_CONCURRENT_DOWNLOADS"
|
||||
)
|
||||
"""Maximum concurrent downloads (limited for development)."""
|
||||
|
||||
download_timeout: int = Field(default=7200, env="DOWNLOAD_TIMEOUT")
|
||||
"""Download timeout in seconds (default: 2 hours)."""
|
||||
|
||||
# ============================================================================
|
||||
# Application Paths
|
||||
# ============================================================================
|
||||
|
||||
anime_directory: str = Field(
|
||||
default="/tmp/aniworld_dev", env="ANIME_DIRECTORY"
|
||||
)
|
||||
"""Directory where anime is stored (development default)."""
|
||||
|
||||
temp_directory: str = Field(
|
||||
default="/tmp/aniworld_dev/temp", env="TEMP_DIRECTORY"
|
||||
)
|
||||
"""Temporary directory for downloads and cache."""
|
||||
|
||||
# ============================================================================
|
||||
# Validators
|
||||
# ============================================================================
|
||||
|
||||
@validator("log_level")
|
||||
@classmethod
|
||||
def validate_log_level(cls, v: str) -> str:
|
||||
"""Validate log level is valid."""
|
||||
valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
||||
if v.upper() not in valid_levels:
|
||||
raise ValueError(
|
||||
f"Invalid log level '{v}'. Must be one of: {valid_levels}"
|
||||
)
|
||||
return v.upper()
|
||||
|
||||
@validator("cors_origins")
|
||||
@classmethod
|
||||
def parse_cors_origins(cls, v: str) -> str:
|
||||
"""Parse comma-separated CORS origins."""
|
||||
if not v:
|
||||
return "http://localhost,http://127.0.0.1"
|
||||
return v
|
||||
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
class Config:
|
||||
"""Pydantic config."""
|
||||
|
||||
env_file = ".env.development"
|
||||
extra = "ignore"
|
||||
case_sensitive = False
|
||||
|
||||
# ============================================================================
|
||||
# Properties
|
||||
# ============================================================================
|
||||
|
||||
@property
|
||||
def parsed_cors_origins(self) -> List[str]:
|
||||
"""Get parsed CORS origins as list."""
|
||||
if not self.cors_origins or self.cors_origins == "*":
|
||||
return ["*"]
|
||||
return [origin.strip() for origin in self.cors_origins.split(",")]
|
||||
|
||||
@property
|
||||
def is_production(self) -> bool:
|
||||
"""Check if running in production mode."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def debug_enabled(self) -> bool:
|
||||
"""Check if debug mode is enabled."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def reload_enabled(self) -> bool:
|
||||
"""Check if auto-reload is enabled."""
|
||||
return True
|
||||
|
||||
|
||||
def get_development_settings() -> DevelopmentSettings:
|
||||
"""
|
||||
Get development settings instance.
|
||||
|
||||
This is a factory function that should be called when settings are needed.
|
||||
|
||||
Returns:
|
||||
DevelopmentSettings instance configured from environment variables
|
||||
"""
|
||||
return DevelopmentSettings()
|
||||
|
||||
|
||||
# Export factory for backward compatibility
|
||||
development_settings = DevelopmentSettings()
|
||||
234
src/server/config/production.py
Normal file
234
src/server/config/production.py
Normal file
@ -0,0 +1,234 @@
|
||||
"""
|
||||
Production environment configuration for Aniworld application.
|
||||
|
||||
This module provides production-specific settings including security hardening,
|
||||
performance optimizations, and operational configurations.
|
||||
|
||||
Environment Variables:
|
||||
JWT_SECRET_KEY: Secret key for JWT token signing (REQUIRED)
|
||||
PASSWORD_SALT: Salt for password hashing (REQUIRED)
|
||||
DATABASE_URL: Production database connection string
|
||||
LOG_LEVEL: Logging level (default: WARNING)
|
||||
CORS_ORIGINS: Comma-separated list of allowed CORS origins
|
||||
API_RATE_LIMIT: API rate limit per minute (default: 60)
|
||||
WORKERS: Number of Uvicorn worker processes (default: 4)
|
||||
WORKER_TIMEOUT: Worker timeout in seconds (default: 120)
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
from pydantic import Field, validator
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class ProductionSettings(BaseSettings):
|
||||
"""Production environment configuration settings."""
|
||||
|
||||
# ============================================================================
|
||||
# Security Settings
|
||||
# ============================================================================
|
||||
|
||||
jwt_secret_key: str = Field(..., env="JWT_SECRET_KEY")
|
||||
"""Secret key for JWT token signing. MUST be set in production."""
|
||||
|
||||
password_salt: str = Field(..., env="PASSWORD_SALT")
|
||||
"""Salt for password hashing. MUST be set in production."""
|
||||
|
||||
master_password_hash: str = Field(..., env="MASTER_PASSWORD_HASH")
|
||||
"""Hash of the master password for authentication."""
|
||||
|
||||
allowed_hosts: List[str] = Field(
|
||||
default=["*"], env="ALLOWED_HOSTS"
|
||||
)
|
||||
"""List of allowed hostnames for CORS and security checks."""
|
||||
|
||||
cors_origins: str = Field(default="", env="CORS_ORIGINS")
|
||||
"""Comma-separated list of allowed CORS origins."""
|
||||
|
||||
# ============================================================================
|
||||
# Database Settings
|
||||
# ============================================================================
|
||||
|
||||
database_url: str = Field(
|
||||
default="postgresql://user:password@localhost/aniworld",
|
||||
env="DATABASE_URL"
|
||||
)
|
||||
"""Database connection URL. Defaults to PostgreSQL for production."""
|
||||
|
||||
database_pool_size: int = Field(default=20, env="DATABASE_POOL_SIZE")
|
||||
"""Database connection pool size."""
|
||||
|
||||
database_max_overflow: int = Field(default=10, env="DATABASE_MAX_OVERFLOW")
|
||||
"""Maximum overflow connections for database pool."""
|
||||
|
||||
database_pool_recycle: int = Field(
|
||||
default=3600, env="DATABASE_POOL_RECYCLE"
|
||||
)
|
||||
"""Recycle database connections every N seconds."""
|
||||
|
||||
# ============================================================================
|
||||
# API Settings
|
||||
# ============================================================================
|
||||
|
||||
api_rate_limit: int = Field(default=60, env="API_RATE_LIMIT")
|
||||
"""API rate limit per minute per IP address."""
|
||||
|
||||
api_timeout: int = Field(default=30, env="API_TIMEOUT")
|
||||
"""API request timeout in seconds."""
|
||||
|
||||
# ============================================================================
|
||||
# Logging Settings
|
||||
# ============================================================================
|
||||
|
||||
log_level: str = Field(default="WARNING", env="LOG_LEVEL")
|
||||
"""Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)."""
|
||||
|
||||
log_file: str = Field(default="logs/production.log", env="LOG_FILE")
|
||||
"""Path to production log file."""
|
||||
|
||||
log_rotation_size: int = Field(default=10_485_760, env="LOG_ROTATION_SIZE")
|
||||
"""Log file rotation size in bytes (default: 10MB)."""
|
||||
|
||||
log_retention_days: int = Field(default=30, env="LOG_RETENTION_DAYS")
|
||||
"""Number of days to retain log files."""
|
||||
|
||||
# ============================================================================
|
||||
# Performance Settings
|
||||
# ============================================================================
|
||||
|
||||
workers: int = Field(default=4, env="WORKERS")
|
||||
"""Number of Uvicorn worker processes."""
|
||||
|
||||
worker_timeout: int = Field(default=120, env="WORKER_TIMEOUT")
|
||||
"""Worker timeout in seconds."""
|
||||
|
||||
max_request_size: int = Field(default=104_857_600, env="MAX_REQUEST_SIZE")
|
||||
"""Maximum request body size in bytes (default: 100MB)."""
|
||||
|
||||
session_timeout_hours: int = Field(default=24, env="SESSION_TIMEOUT_HOURS")
|
||||
"""Session timeout in hours."""
|
||||
|
||||
# ============================================================================
|
||||
# Provider Settings
|
||||
# ============================================================================
|
||||
|
||||
default_provider: str = Field(
|
||||
default="aniworld.to", env="DEFAULT_PROVIDER"
|
||||
)
|
||||
"""Default content provider."""
|
||||
|
||||
provider_timeout: int = Field(default=30, env="PROVIDER_TIMEOUT")
|
||||
"""Provider request timeout in seconds."""
|
||||
|
||||
provider_retries: int = Field(default=3, env="PROVIDER_RETRIES")
|
||||
"""Number of retry attempts for provider requests."""
|
||||
|
||||
# ============================================================================
|
||||
# Download Settings
|
||||
# ============================================================================
|
||||
|
||||
max_concurrent_downloads: int = Field(
|
||||
default=3, env="MAX_CONCURRENT_DOWNLOADS"
|
||||
)
|
||||
"""Maximum concurrent downloads."""
|
||||
|
||||
download_timeout: int = Field(default=3600, env="DOWNLOAD_TIMEOUT")
|
||||
"""Download timeout in seconds (default: 1 hour)."""
|
||||
|
||||
# ============================================================================
|
||||
# Application Paths
|
||||
# ============================================================================
|
||||
|
||||
anime_directory: str = Field(..., env="ANIME_DIRECTORY")
|
||||
"""Directory where anime is stored."""
|
||||
|
||||
temp_directory: str = Field(default="/tmp/aniworld", env="TEMP_DIRECTORY")
|
||||
"""Temporary directory for downloads and cache."""
|
||||
|
||||
# ============================================================================
|
||||
# Validators
|
||||
# ============================================================================
|
||||
|
||||
@validator("log_level")
|
||||
@classmethod
|
||||
def validate_log_level(cls, v: str) -> str:
|
||||
"""Validate log level is valid."""
|
||||
valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
||||
if v.upper() not in valid_levels:
|
||||
raise ValueError(
|
||||
f"Invalid log level '{v}'. Must be one of: {valid_levels}"
|
||||
)
|
||||
return v.upper()
|
||||
|
||||
@validator("database_url")
|
||||
@classmethod
|
||||
def validate_database_url(cls, v: str) -> str:
|
||||
"""Validate database URL is set and not SQLite."""
|
||||
if not v or v.startswith("sqlite"):
|
||||
raise ValueError(
|
||||
"Production database must not use SQLite. "
|
||||
"Use PostgreSQL or MySQL instead."
|
||||
)
|
||||
return v
|
||||
|
||||
@validator("cors_origins")
|
||||
@classmethod
|
||||
def parse_cors_origins(cls, v: str) -> str:
|
||||
"""Parse comma-separated CORS origins."""
|
||||
if not v:
|
||||
return ""
|
||||
return v
|
||||
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
class Config:
|
||||
"""Pydantic config."""
|
||||
|
||||
env_file = ".env.production"
|
||||
extra = "ignore"
|
||||
case_sensitive = False
|
||||
|
||||
# ============================================================================
|
||||
# Properties
|
||||
# ============================================================================
|
||||
|
||||
@property
|
||||
def parsed_cors_origins(self) -> List[str]:
|
||||
"""Get parsed CORS origins as list."""
|
||||
if not self.cors_origins:
|
||||
return ["http://localhost", "http://127.0.0.1"]
|
||||
return [origin.strip() for origin in self.cors_origins.split(",")]
|
||||
|
||||
@property
|
||||
def is_production(self) -> bool:
|
||||
"""Check if running in production mode."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def debug_enabled(self) -> bool:
|
||||
"""Check if debug mode is enabled."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def reload_enabled(self) -> bool:
|
||||
"""Check if auto-reload is enabled."""
|
||||
return False
|
||||
|
||||
|
||||
def get_production_settings() -> ProductionSettings:
|
||||
"""
|
||||
Get production settings instance.
|
||||
|
||||
This is a factory function that should be called when settings are needed,
|
||||
rather than instantiating at module level to avoid requiring all
|
||||
environment variables at import time.
|
||||
|
||||
Returns:
|
||||
ProductionSettings instance configured from environment variables
|
||||
|
||||
Raises:
|
||||
ValidationError: If required environment variables are missing
|
||||
"""
|
||||
return ProductionSettings()
|
||||
Loading…
x
Reference in New Issue
Block a user