diff --git a/DATABASE_IMPLEMENTATION_SUMMARY.md b/DATABASE_IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 3c39f8b..0000000 --- a/DATABASE_IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -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 diff --git a/data/download_queue.json b/data/download_queue.json index 8c3bea3..8bfe8f7 100644 --- a/data/download_queue.json +++ b/data/download_queue.json @@ -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" } \ No newline at end of file diff --git a/instructions.md b/instructions.md index 51779fb..c077933 100644 --- a/instructions.md +++ b/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. diff --git a/requirements.txt b/requirements.txt index ba61ab3..fe6fe32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/scripts/setup.py b/scripts/setup.py new file mode 100644 index 0000000..26a999d --- /dev/null +++ b/scripts/setup.py @@ -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) diff --git a/scripts/start.sh b/scripts/start.sh new file mode 100644 index 0000000..186a105 --- /dev/null +++ b/scripts/start.sh @@ -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 "$@" diff --git a/src/server/config/__init__.py b/src/server/config/__init__.py new file mode 100644 index 0000000..322fb17 --- /dev/null +++ b/src/server/config/__init__.py @@ -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", +] diff --git a/src/server/config/development.py b/src/server/config/development.py new file mode 100644 index 0000000..761a0d0 --- /dev/null +++ b/src/server/config/development.py @@ -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() diff --git a/src/server/config/production.py b/src/server/config/production.py new file mode 100644 index 0000000..d68f6dc --- /dev/null +++ b/src/server/config/production.py @@ -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()