Compare commits
15 Commits
8b8dd68b34
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc5796d989 | ||
|
|
c073006592 | ||
|
|
ed7165632a | ||
|
|
d809dd6753 | ||
|
|
ea05c847ba | ||
|
|
f5e0bba298 | ||
|
|
9a35a57c7f | ||
|
|
cd5f0a54ac | ||
|
|
d1e5fc1003 | ||
|
|
7d03f5b40c | ||
|
|
795016a60c | ||
|
|
b6ea2a4ff1 | ||
|
|
5f89af44aa | ||
|
|
39d408d882 | ||
|
|
5bdde5fee1 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
.env
|
.env
|
||||||
|
preferred_sessions.json
|
||||||
log/
|
log/
|
||||||
!log/.gitkeep
|
!log/.gitkeep
|
||||||
|
|
||||||
|
|||||||
63
AGENTS.md
Normal file
63
AGENTS.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Qwen Code Agent Configuration
|
||||||
|
|
||||||
|
This file configures the behavior and preferences for the Qwen Code agent for this specific project.
|
||||||
|
|
||||||
|
## Project Context
|
||||||
|
- **Name**: CrossFit Scheduler
|
||||||
|
- **Primary Language**: Python
|
||||||
|
- **Key Technologies**: Python scripts, JSON, Docker
|
||||||
|
- **Purpose**: Automates booking CrossFit classes based on user preferences.
|
||||||
|
|
||||||
|
## Agent Interaction Preferences
|
||||||
|
|
||||||
|
### Code Style & Conventions
|
||||||
|
- Follow PEP 8 for Python code.
|
||||||
|
- Use clear, descriptive variable and function names.
|
||||||
|
- Prefer explicit over implicit code.
|
||||||
|
- Use docstrings for modules, classes, and functions.
|
||||||
|
- Keep functions small and focused on a single task.
|
||||||
|
|
||||||
|
### Project Structure Awareness
|
||||||
|
- `main.py`: Entry point for the application.
|
||||||
|
- `execute_book_session.py`: Alternative execution script for booking sessions.
|
||||||
|
- `preferred_sessions.json`: User preferences for session booking.
|
||||||
|
- `src/`: Directory for source code modules.
|
||||||
|
- `auth.py`: Authentication handling with bearer token management.
|
||||||
|
- `booker.py`: Main booking logic with time-based booking windows.
|
||||||
|
- `session_manager.py`: Session fetching, filtering, and booking operations.
|
||||||
|
- `session_notifier.py`: Email and Telegram notifications for booking status.
|
||||||
|
- `session_config.py`: Configuration for preferred session matching.
|
||||||
|
- `test_book_session.sh`: Shell script for testing session booking.
|
||||||
|
- `requirements.txt`: Python dependencies.
|
||||||
|
- `Dockerfile` and `docker-compose.yml`: Container configuration.
|
||||||
|
|
||||||
|
### Preferred Tools & Commands
|
||||||
|
- Use `pytest` for running tests.
|
||||||
|
- Use `docker compose` for container orchestration.
|
||||||
|
- Use `python` for running scripts.
|
||||||
|
- Refer to `requirements.txt` for dependencies.
|
||||||
|
|
||||||
|
### Communication Style
|
||||||
|
- Be concise and direct.
|
||||||
|
- Use markdown for formatting responses.
|
||||||
|
- Prioritize clarity and correctness.
|
||||||
|
- Avoid unnecessary explanations or chit-chat.
|
||||||
|
|
||||||
|
### Testing & Verification
|
||||||
|
- Always run tests after making changes.
|
||||||
|
- Use `pytest.ini` for test configuration.
|
||||||
|
- Ensure Docker containers are properly configured before running.
|
||||||
|
- Verify JSON files are correctly formatted.
|
||||||
|
|
||||||
|
### Security & Best Practices
|
||||||
|
- Never commit sensitive information like passwords or API keys.
|
||||||
|
- Use `.env` files for environment variables.
|
||||||
|
- Follow Docker best practices for containerization.
|
||||||
|
- Keep dependencies up to date.
|
||||||
|
|
||||||
|
## Custom Instructions
|
||||||
|
- When modifying `preferred_sessions.json`, ensure the structure remains valid.
|
||||||
|
- When writing shell scripts, ensure they are executable and follow best practices.
|
||||||
|
- When creating new Python files, ensure they are properly formatted and tested.
|
||||||
|
- Environment variables required: `CROSSFIT_USERNAME`, `CROSSFIT_PASSWORD`, plus optional email/telegram settings.
|
||||||
|
- Booking window controlled by `TARGET_RESERVATION_TIME` (default: "20:01") and `BOOKING_WINDOW_END_DELTA_MINUTES` (default: 10).
|
||||||
54
README.md
54
README.md
@@ -12,12 +12,16 @@ This is a Python application for managing Crossfit bookings and notifications. T
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Docker
|
- Python 3.8+
|
||||||
- Docker Compose
|
- Docker (optional)
|
||||||
|
- Docker Compose (optional)
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
1. Create a `.env` file based on `.env.example` and fill in the required credentials.
|
1. Create a `.env` file based on `.env.example` and fill in the required credentials.
|
||||||
|
|
||||||
|
### Running with Docker (Recommended)
|
||||||
|
|
||||||
2. Build and run the application using Docker Compose:
|
2. Build and run the application using Docker Compose:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -26,6 +30,20 @@ docker-compose up --build
|
|||||||
|
|
||||||
3. The application will run in a Docker container, and the logs will be stored in the `./log` directory.
|
3. The application will run in a Docker container, and the logs will be stored in the `./log` directory.
|
||||||
|
|
||||||
|
### Manual Setup
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run the application:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
The application will automatically check for available sessions and book them based on your preferences. It will send notifications via email and Telegram when a booking is successful.
|
The application will automatically check for available sessions and book them based on your preferences. It will send notifications via email and Telegram when a booking is successful.
|
||||||
@@ -76,32 +94,46 @@ The application will automatically load the preferred sessions from this file. I
|
|||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
|
- `main.py`: Main entry point for the application
|
||||||
|
- `src/`: Source code directory
|
||||||
|
- `auth.py`: Authentication handling
|
||||||
|
- `booker.py`: Main booking logic
|
||||||
|
- `session_manager.py`: Session management
|
||||||
|
- `session_notifier.py`: Notification handling
|
||||||
|
- `session_config.py`: Session configuration
|
||||||
|
- `tools/`: Utility scripts
|
||||||
|
- `scripts/`: Additional scripts
|
||||||
|
- `test/`: Test files
|
||||||
- `Dockerfile`: Docker image definition
|
- `Dockerfile`: Docker image definition
|
||||||
- `docker-compose.yml`: Docker Compose service definition
|
- `docker-compose.yml`: Docker Compose service definition
|
||||||
- `.env.example`: Example environment variables file
|
- `.env.example`: Example environment variables file
|
||||||
- `.dockerignore`: Docker ignore file
|
- `.dockerignore`: Docker ignore file
|
||||||
- `.gitignore`: Git ignore file
|
- `.gitignore`: Git ignore file
|
||||||
- `book_crossfit.py`: Main application script
|
- `preferred_sessions.json.example`: Example configuration file for preferred sessions
|
||||||
- `crossfit_booker.py`: Crossfit booking script
|
|
||||||
- `session_notifier.py`: Session notification script
|
|
||||||
- `preferred_sessions.json`: Configuration file for preferred sessions
|
|
||||||
- `requirements.txt`: Python dependencies
|
- `requirements.txt`: Python dependencies
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
|
├── main.py
|
||||||
|
├── src/
|
||||||
|
│ ├── auth.py
|
||||||
|
│ ├── booker.py
|
||||||
|
│ ├── session_manager.py
|
||||||
|
│ ├── session_notifier.py
|
||||||
|
│ └── session_config.py
|
||||||
|
├── tools/
|
||||||
|
├── scripts/
|
||||||
|
├── test/
|
||||||
├── Dockerfile
|
├── Dockerfile
|
||||||
├── docker-compose.yml
|
├── docker-compose.yml
|
||||||
├── .env.example
|
├── .env.example
|
||||||
├── .dockerignore
|
├── .dockerignore
|
||||||
├── .gitignore
|
├── .gitignore
|
||||||
├── book_crossfit.py
|
├── preferred_sessions.json.example
|
||||||
├── crossfit_booker.py
|
|
||||||
├── session_notifier.py
|
|
||||||
├── requirements.txt
|
├── requirements.txt
|
||||||
├── preferred_sessions.json
|
└── log/
|
||||||
└── log
|
|
||||||
└── crossfit_booking.log
|
└── crossfit_booking.log
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
9
ascii.md
Normal file
9
ascii.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
▄▄▄ ▄▄▄▄▄▄ ▀
|
||||||
|
▄▀ ▀ ▄ ▄▄ ▄▄▄ ▄▄▄ ▄▄▄ █ ▄▄▄ ▄ ▄
|
||||||
|
█ █▀ ▀ █▀ ▀█ █ ▀ █ ▀ █▄▄▄▄▄ █ █▄█
|
||||||
|
█ █ █ █ ▀▀▀▄ ▀▀▀▄ █ █ ▄█▄
|
||||||
|
▀▄▄▄▀ █ ▀█▄█▀ ▀▄▄▄▀ ▀▄▄▄▀ █ ▄▄█▄▄ ▄▀ ▀▄
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: crossfit-booker
|
# container_name: crossfit-booker
|
||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
- CROSSFIT_USERNAME=${CROSSFIT_USERNAME}
|
- CROSSFIT_USERNAME=${CROSSFIT_USERNAME}
|
||||||
@@ -18,5 +18,5 @@ services:
|
|||||||
- TELEGRAM_TOKEN=${TELEGRAM_TOKEN}
|
- TELEGRAM_TOKEN=${TELEGRAM_TOKEN}
|
||||||
- TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID}
|
- TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID}
|
||||||
volumes:
|
volumes:
|
||||||
- ./log:/app/log
|
- ./:/app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Script to demonstrate how to execute the book_session method from crossfit_booker.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
from crossfit_booker import CrossFitBooker
|
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Check if a session ID was provided as an argument
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("Usage: python execute_book_session.py <session_id>")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
session_id = sys.argv[1]
|
|
||||||
|
|
||||||
# Create an instance of CrossFitBooker
|
|
||||||
booker = CrossFitBooker()
|
|
||||||
|
|
||||||
# Login to authenticate
|
|
||||||
print("Attempting to authenticate...")
|
|
||||||
if not booker.login():
|
|
||||||
print("Failed to authenticate. Please check your credentials and try again.")
|
|
||||||
sys.exit(1)
|
|
||||||
print("Authentication successful!")
|
|
||||||
|
|
||||||
# Book the session
|
|
||||||
print(f"Attempting to book session with ID: {session_id}")
|
|
||||||
success = booker.book_session(session_id)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
print(f"Successfully booked session {session_id}")
|
|
||||||
else:
|
|
||||||
print(f"Failed to book session {session_id}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
try:
|
|
||||||
main()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"An error occurred: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
57
main.py
57
main.py
@@ -5,11 +5,18 @@ This script initializes the CrossFitBooker and starts the booking process.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from src.crossfit_booker import CrossFitBooker
|
import os
|
||||||
|
from src.auth import AuthHandler
|
||||||
|
from src.booker import Booker
|
||||||
|
from src.session_notifier import SessionNotifier
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""
|
"""
|
||||||
Main function to initialize the CrossFitBooker and start the booking process.
|
Main function to initialize the Booker and start the booking process.
|
||||||
"""
|
"""
|
||||||
# Set up logging
|
# Set up logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -20,11 +27,51 @@ def main():
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize the CrossFitBooker
|
# Display ASCII art and username
|
||||||
booker = CrossFitBooker()
|
try:
|
||||||
|
with open('ascii.md', 'r') as f:
|
||||||
|
ascii_art = f.read()
|
||||||
|
print(ascii_art, flush=True)
|
||||||
|
username = os.environ.get("CROSSFIT_USERNAME")
|
||||||
|
print(f"Username: {username}", flush=True)
|
||||||
|
print(flush=True)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error displaying ASCII: {e}")
|
||||||
|
|
||||||
|
# Initialize components
|
||||||
|
auth_handler = AuthHandler(
|
||||||
|
str(os.environ.get("CROSSFIT_USERNAME")),
|
||||||
|
str(os.environ.get("CROSSFIT_PASSWORD"))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize notification system
|
||||||
|
email_credentials = {
|
||||||
|
"from": os.environ.get("EMAIL_FROM"),
|
||||||
|
"to": os.environ.get("EMAIL_TO"),
|
||||||
|
"password": os.environ.get("EMAIL_PASSWORD")
|
||||||
|
}
|
||||||
|
|
||||||
|
telegram_credentials = {
|
||||||
|
"token": os.environ.get("TELEGRAM_TOKEN"),
|
||||||
|
"chat_id": os.environ.get("TELEGRAM_CHAT_ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
enable_email = os.environ.get("ENABLE_EMAIL_NOTIFICATIONS", "true").lower() in ("true", "1", "yes")
|
||||||
|
enable_telegram = os.environ.get("ENABLE_TELEGRAM_NOTIFICATIONS", "true").lower() in ("true", "1", "yes")
|
||||||
|
|
||||||
|
notifier = SessionNotifier(
|
||||||
|
email_credentials,
|
||||||
|
telegram_credentials,
|
||||||
|
enable_email=enable_email,
|
||||||
|
enable_telegram=enable_telegram
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize the Booker
|
||||||
|
booker = Booker(auth_handler, notifier)
|
||||||
|
|
||||||
# Run the booking process
|
# Run the booking process
|
||||||
booker.run()
|
import asyncio
|
||||||
|
asyncio.run(booker.run())
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"day_of_week": 2,
|
"day_of_week": 0,
|
||||||
"start_time": "18:30",
|
"start_time": "17:45",
|
||||||
"session_name_contains": "CONDITIONING"
|
"session_name_contains": "WEIGHTLIFTING LOUVRE 3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"day_of_week": 4,
|
"day_of_week": 4,
|
||||||
"start_time": "17:00",
|
"start_time": "17:00",
|
||||||
"session_name_contains": "WEIGHTLIFTING"
|
"session_name_contains": "CONDITIONING LOUVRE 3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"day_of_week": 5,
|
"day_of_week": 5,
|
||||||
80
scripts/execute_book_session.py
Executable file
80
scripts/execute_book_session.py
Executable file
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Script to demonstrate how to execute the book_session method from crossfit_booker.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from src.auth import AuthHandler
|
||||||
|
from src.booker import Booker
|
||||||
|
from src.session_notifier import SessionNotifier
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Check if a session ID was provided as an argument
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python execute_book_session.py <session_id>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
session_id = sys.argv[1]
|
||||||
|
|
||||||
|
# Initialize components
|
||||||
|
auth_handler = AuthHandler(
|
||||||
|
os.environ.get("CROSSFIT_USERNAME"),
|
||||||
|
os.environ.get("CROSSFIT_PASSWORD")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize notification system (minimal for single session booking)
|
||||||
|
email_credentials = {
|
||||||
|
"from": os.environ.get("EMAIL_FROM"),
|
||||||
|
"to": os.environ.get("EMAIL_TO"),
|
||||||
|
"password": os.environ.get("EMAIL_PASSWORD")
|
||||||
|
}
|
||||||
|
|
||||||
|
telegram_credentials = {
|
||||||
|
"token": os.environ.get("TELEGRAM_TOKEN"),
|
||||||
|
"chat_id": os.environ.get("TELEGRAM_CHAT_ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
enable_email = os.environ.get("ENABLE_EMAIL_NOTIFICATIONS", "true").lower() in ("true", "1", "yes")
|
||||||
|
enable_telegram = os.environ.get("ENABLE_TELEGRAM_NOTIFICATIONS", "true").lower() in ("true", "1", "yes")
|
||||||
|
|
||||||
|
notifier = SessionNotifier(
|
||||||
|
email_credentials,
|
||||||
|
telegram_credentials,
|
||||||
|
enable_email=enable_email,
|
||||||
|
enable_telegram=enable_telegram
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create an instance of Booker
|
||||||
|
booker = Booker(auth_handler, notifier)
|
||||||
|
|
||||||
|
# Login to authenticate
|
||||||
|
print("Attempting to authenticate...")
|
||||||
|
if not auth_handler.login():
|
||||||
|
print("Failed to authenticate. Please check your credentials and try again.")
|
||||||
|
sys.exit(1)
|
||||||
|
print("Authentication successful!")
|
||||||
|
|
||||||
|
# Book the session
|
||||||
|
print(f"Attempting to book session with ID: {session_id}")
|
||||||
|
success = booker.book_session(session_id)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f"Successfully booked session {session_id}")
|
||||||
|
else:
|
||||||
|
print(f"Failed to book session {session_id}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An error occurred: {e}")
|
||||||
|
sys.exit(1)
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
# Import all public modules to make them available as src.module
|
# Import all public modules to make them available as src.module
|
||||||
from .auth import AuthHandler
|
from .auth import AuthHandler
|
||||||
from .booker import Booker
|
from .booker import Booker
|
||||||
from .crossfit_booker import CrossFitBooker
|
|
||||||
from .session_config import PREFERRED_SESSIONS
|
from .session_config import PREFERRED_SESSIONS
|
||||||
from .session_manager import SessionManager
|
from .session_manager import SessionManager
|
||||||
from .session_notifier import SessionNotifier
|
from .session_notifier import SessionNotifier
|
||||||
@@ -11,7 +10,6 @@ from .session_notifier import SessionNotifier
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"AuthHandler",
|
"AuthHandler",
|
||||||
"Booker",
|
"Booker",
|
||||||
"CrossFitBooker",
|
|
||||||
"PREFERRED_SESSIONS",
|
"PREFERRED_SESSIONS",
|
||||||
"SessionManager",
|
"SessionManager",
|
||||||
"SessionNotifier"
|
"SessionNotifier"
|
||||||
|
|||||||
@@ -77,6 +77,12 @@ class AuthHandler:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
login_data: Dict[str, Any] = response.json()
|
login_data: Dict[str, Any] = response.json()
|
||||||
|
# Try to find id_user in the nested structure
|
||||||
|
try:
|
||||||
|
# First try the new structure: data.user.applications[0].users[0].id_user
|
||||||
|
self.user_id = str(login_data["data"]["user"]["applications"][0]["users"][0]["id_user"])
|
||||||
|
except (KeyError, IndexError, TypeError):
|
||||||
|
# Fallback to old structure if it exists
|
||||||
self.user_id = str(login_data["data"]["user"]["id_user"])
|
self.user_id = str(login_data["data"]["user"]["id_user"])
|
||||||
except (KeyError, ValueError) as e:
|
except (KeyError, ValueError) as e:
|
||||||
logging.error(f"Error during login: {str(e)} - Response: {response.text}")
|
logging.error(f"Error during login: {str(e)} - Response: {response.text}")
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
from crossfit_booker import CrossFitBooker
|
from src.auth import AuthHandler
|
||||||
|
from src.booker import Booker
|
||||||
|
from src.session_notifier import SessionNotifier
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Configure logging once at script startup
|
# Configure logging once at script startup
|
||||||
@@ -19,16 +26,45 @@ if __name__ == "__main__":
|
|||||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||||
logging.info("Logging enhanced with request library noise reduction")
|
logging.info("Logging enhanced with request library noise reduction")
|
||||||
|
|
||||||
# Create an instance of the CrossFitBooker class
|
# Initialize components
|
||||||
booker = CrossFitBooker()
|
auth_handler = AuthHandler(
|
||||||
|
os.environ.get("CROSSFIT_USERNAME"),
|
||||||
|
os.environ.get("CROSSFIT_PASSWORD")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize notification system
|
||||||
|
email_credentials = {
|
||||||
|
"from": os.environ.get("EMAIL_FROM"),
|
||||||
|
"to": os.environ.get("EMAIL_TO"),
|
||||||
|
"password": os.environ.get("EMAIL_PASSWORD")
|
||||||
|
}
|
||||||
|
|
||||||
|
telegram_credentials = {
|
||||||
|
"token": os.environ.get("TELEGRAM_TOKEN"),
|
||||||
|
"chat_id": os.environ.get("TELEGRAM_CHAT_ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
enable_email = os.environ.get("ENABLE_EMAIL_NOTIFICATIONS", "true").lower() in ("true", "1", "yes")
|
||||||
|
enable_telegram = os.environ.get("ENABLE_TELEGRAM_NOTIFICATIONS", "true").lower() in ("true", "1", "yes")
|
||||||
|
|
||||||
|
notifier = SessionNotifier(
|
||||||
|
email_credentials,
|
||||||
|
telegram_credentials,
|
||||||
|
enable_email=enable_email,
|
||||||
|
enable_telegram=enable_telegram
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create an instance of the Booker class
|
||||||
|
booker = Booker(auth_handler, notifier)
|
||||||
|
|
||||||
# Attempt to log in to the CrossFit booking system
|
# Attempt to log in to the CrossFit booking system
|
||||||
# TODO: Make a authentication during running request to not get kicked out
|
# TODO: Make a authentication during running request to not get kicked out
|
||||||
if not booker.login():
|
if not auth_handler.login():
|
||||||
# If login fails, log the error and exit the script
|
# If login fails, log the error and exit the script
|
||||||
logging.error("Failed to login - Traceback: %s", traceback.format_exc())
|
logging.error("Failed to login - Traceback: %s", traceback.format_exc())
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
# Start the continuous booking loop
|
# Start the continuous booking loop
|
||||||
booker.run()
|
import asyncio
|
||||||
|
asyncio.run(booker.run())
|
||||||
logging.info("Script completed")
|
logging.info("Script completed")
|
||||||
|
|||||||
@@ -162,13 +162,13 @@ class Booker:
|
|||||||
logging.info(f"Successfully booked {session_type} session at {session_time}")
|
logging.info(f"Successfully booked {session_type} session at {session_time}")
|
||||||
|
|
||||||
# Notify about booked session
|
# Notify about booked session
|
||||||
session_details = f"{session['name_activity']} at {session_time.strftime('%Y-%m-%d %H:%M')}"
|
session_details = f"ID: {session['id_activity_calendar']}, Name: {session['name_activity']}, Date: {session_time.strftime('%Y-%m-%d %H:%M')}"
|
||||||
await self.notifier.notify_session_booking(session_details)
|
await self.notifier.notify_session_booking(session_details)
|
||||||
else:
|
else:
|
||||||
logging.error(f"Failed to book {session_type} session at {session_time}")
|
logging.error(f"Failed to book {session_type} session at {session_time}")
|
||||||
|
|
||||||
# Notify about failed booking
|
# Notify about failed booking
|
||||||
session_details = f"{session['name_activity']} at {session_time.strftime('%Y-%m-%d %H:%M')}"
|
session_details = f"ID: {session['id_activity_calendar']}, Name: {session['name_activity']}, Date: {session_time.strftime('%Y-%m-%d %H:%M')}"
|
||||||
await self.notifier.notify_impossible_booking(session_details)
|
await self.notifier.notify_impossible_booking(session_details)
|
||||||
logging.info(f"Notified about impossible booking for {session_type} session at {session_time}")
|
logging.info(f"Notified about impossible booking for {session_type} session at {session_time}")
|
||||||
|
|
||||||
@@ -192,8 +192,6 @@ class Booker:
|
|||||||
|
|
||||||
# Parse TARGET_RESERVATION_TIME to get the target hour and minute
|
# Parse TARGET_RESERVATION_TIME to get the target hour and minute
|
||||||
target_hour, target_minute = map(int, TARGET_RESERVATION_TIME.split(":"))
|
target_hour, target_minute = map(int, TARGET_RESERVATION_TIME.split(":"))
|
||||||
target_time = datetime.now(tz).replace(hour=target_hour, minute=target_minute, second=0, microsecond=0)
|
|
||||||
booking_window_end = target_time + timedelta(minutes=BOOKING_WINDOW_END_DELTA_MINUTES)
|
|
||||||
|
|
||||||
# Initial login
|
# Initial login
|
||||||
if not self.auth_handler.login():
|
if not self.auth_handler.login():
|
||||||
@@ -206,15 +204,19 @@ class Booker:
|
|||||||
current_time: datetime = datetime.now(tz)
|
current_time: datetime = datetime.now(tz)
|
||||||
logging.info(f"Current time: {current_time}")
|
logging.info(f"Current time: {current_time}")
|
||||||
|
|
||||||
|
# Recalculate target time and end time for the current day
|
||||||
|
today_target_time = current_time.replace(hour=target_hour, minute=target_minute, second=0, microsecond=0)
|
||||||
|
booking_window_end = today_target_time + timedelta(minutes=BOOKING_WINDOW_END_DELTA_MINUTES)
|
||||||
|
|
||||||
# Only book sessions if current time is within the booking window
|
# Only book sessions if current time is within the booking window
|
||||||
if target_time <= current_time <= booking_window_end:
|
if today_target_time <= current_time <= booking_window_end:
|
||||||
# Run booking cycle to check for preferred sessions and book
|
# Run booking cycle to check for preferred sessions and book
|
||||||
await self.booker(current_time)
|
await self.booker(current_time)
|
||||||
# Wait for a short time before next check
|
# Wait for a short time before next check
|
||||||
time.sleep(60)
|
time.sleep(60)
|
||||||
else:
|
else:
|
||||||
# Display message when outside booking window
|
# Display message when outside booking window
|
||||||
# logging.info(f"Not booking now - current time {current_time} is outside the booking window ({target_time} to {booking_window_end})")
|
# logging.info(f"Not booking now - current time {current_time} is outside the booking window ({today_target_time} to {booking_window_end})")
|
||||||
# Check again in 5 minutes if outside booking window
|
# Check again in 5 minutes if outside booking window
|
||||||
time.sleep(300)
|
time.sleep(300)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -1,193 +0,0 @@
|
|||||||
# Native modules
|
|
||||||
import os
|
|
||||||
from typing import Dict, Any, Optional, List
|
|
||||||
from datetime import date, datetime
|
|
||||||
|
|
||||||
# Third-party modules
|
|
||||||
import requests
|
|
||||||
|
|
||||||
# Import the AuthHandler class
|
|
||||||
from src.auth import AuthHandler
|
|
||||||
|
|
||||||
# Import the SessionManager class
|
|
||||||
from src.session_manager import SessionManager
|
|
||||||
|
|
||||||
# Import the Booker class
|
|
||||||
from src.booker import Booker
|
|
||||||
|
|
||||||
# Import the SessionNotifier class
|
|
||||||
from src.session_notifier import SessionNotifier
|
|
||||||
|
|
||||||
# Load environment variables
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
USERNAME = os.environ.get("CROSSFIT_USERNAME")
|
|
||||||
PASSWORD = os.environ.get("CROSSFIT_PASSWORD")
|
|
||||||
|
|
||||||
if not all([USERNAME, PASSWORD]):
|
|
||||||
raise ValueError("Missing environment variables: CROSSFIT_USERNAME and/or CROSSFIT_PASSWORD")
|
|
||||||
|
|
||||||
class CrossFitBooker:
|
|
||||||
"""
|
|
||||||
A simple orchestrator class for the CrossFit booking system.
|
|
||||||
|
|
||||||
This class is responsible for initializing and coordinating the other components
|
|
||||||
(AuthHandler, SessionManager, and Booker) and provides a unified interface for
|
|
||||||
interacting with the booking system.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
"""
|
|
||||||
Initialize the CrossFitBooker with necessary components.
|
|
||||||
"""
|
|
||||||
# Initialize the AuthHandler with credentials from environment variables
|
|
||||||
self.auth_handler = AuthHandler(USERNAME, PASSWORD)
|
|
||||||
|
|
||||||
# Initialize the SessionManager with the AuthHandler
|
|
||||||
self.session_manager = SessionManager(self.auth_handler)
|
|
||||||
|
|
||||||
# Initialize the SessionNotifier with credentials from environment variables
|
|
||||||
email_credentials = {
|
|
||||||
"from": os.environ.get("EMAIL_FROM"),
|
|
||||||
"to": os.environ.get("EMAIL_TO"),
|
|
||||||
"password": os.environ.get("EMAIL_PASSWORD")
|
|
||||||
}
|
|
||||||
|
|
||||||
telegram_credentials = {
|
|
||||||
"token": os.environ.get("TELEGRAM_TOKEN"),
|
|
||||||
"chat_id": os.environ.get("TELEGRAM_CHAT_ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get notification settings from environment variables
|
|
||||||
enable_email = os.environ.get("ENABLE_EMAIL_NOTIFICATIONS", "true").lower() in ("true", "1", "yes")
|
|
||||||
enable_telegram = os.environ.get("ENABLE_TELEGRAM_NOTIFICATIONS", "true").lower() in ("true", "1", "yes")
|
|
||||||
|
|
||||||
self.notifier = SessionNotifier(
|
|
||||||
email_credentials,
|
|
||||||
telegram_credentials,
|
|
||||||
enable_email=enable_email,
|
|
||||||
enable_telegram=enable_telegram
|
|
||||||
)
|
|
||||||
|
|
||||||
# Initialize the Booker with the AuthHandler and SessionNotifier
|
|
||||||
self.booker = Booker(self.auth_handler, self.notifier)
|
|
||||||
|
|
||||||
# Initialize a session for direct API calls
|
|
||||||
self.session = requests.Session()
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
"""
|
|
||||||
Start the booking process.
|
|
||||||
|
|
||||||
This method initiates the booking process by running the Booker's main execution loop.
|
|
||||||
"""
|
|
||||||
import asyncio
|
|
||||||
try:
|
|
||||||
asyncio.run(self.booker.run())
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error in booking process: {str(e)}")
|
|
||||||
|
|
||||||
def get_auth_headers(self) -> Dict[str, str]:
|
|
||||||
"""
|
|
||||||
Return headers with authorization from the AuthHandler.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, str]: Headers dictionary with authorization if available.
|
|
||||||
"""
|
|
||||||
return self.auth_handler.get_auth_headers()
|
|
||||||
|
|
||||||
def login(self) -> bool:
|
|
||||||
"""
|
|
||||||
Authenticate and get the bearer token.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if login is successful, False otherwise.
|
|
||||||
"""
|
|
||||||
return self.auth_handler.login()
|
|
||||||
|
|
||||||
def get_available_sessions(self, start_date: date, end_date: date) -> Optional[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Fetch available sessions from the API.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start_date (date): Start date for fetching sessions.
|
|
||||||
end_date (date): End date for fetching sessions.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Optional[Dict[str, Any]]: Dictionary containing available sessions if successful, None otherwise.
|
|
||||||
"""
|
|
||||||
return self.session_manager.get_available_sessions(start_date, end_date)
|
|
||||||
|
|
||||||
def book_session(self, session_id: str) -> bool:
|
|
||||||
"""
|
|
||||||
Book a specific session.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session_id (str): ID of the session to book.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if booking is successful, False otherwise.
|
|
||||||
"""
|
|
||||||
return self.session_manager.book_session(session_id)
|
|
||||||
|
|
||||||
def get_booked_sessions(self) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Get a list of booked sessions.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of dictionaries containing information about the booked sessions.
|
|
||||||
"""
|
|
||||||
return self.session_manager.get_booked_sessions()
|
|
||||||
|
|
||||||
def is_session_bookable(self, session: Dict[str, Any], current_time: datetime) -> bool:
|
|
||||||
"""
|
|
||||||
Check if a session is bookable based on user_info.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session (Dict[str, Any]): Session data.
|
|
||||||
current_time (datetime): Current time for comparison.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if the session is bookable, False otherwise.
|
|
||||||
"""
|
|
||||||
return self.session_manager.is_session_bookable(session, current_time)
|
|
||||||
|
|
||||||
def matches_preferred_session(self, session: Dict[str, Any], current_time: datetime) -> bool:
|
|
||||||
"""
|
|
||||||
Check if session matches one of your preferred sessions with exact matching.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session (Dict[str, Any]): Session data.
|
|
||||||
current_time (datetime): Current time for comparison.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if the session matches a preferred session, False otherwise.
|
|
||||||
"""
|
|
||||||
return self.session_manager.matches_preferred_session(session, current_time)
|
|
||||||
|
|
||||||
def display_upcoming_sessions(self, sessions: List[Dict[str, Any]], current_time: datetime) -> None:
|
|
||||||
"""
|
|
||||||
Display upcoming sessions with ID, name, date, and time.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
sessions (List[Dict[str, Any]]): List of session data.
|
|
||||||
current_time (datetime): Current time for comparison.
|
|
||||||
"""
|
|
||||||
self.session_manager.display_upcoming_sessions(sessions, current_time)
|
|
||||||
|
|
||||||
async def run_booking_cycle(self, current_time: datetime) -> None:
|
|
||||||
"""
|
|
||||||
Run one cycle of checking and booking sessions.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current_time (datetime): Current time for comparison.
|
|
||||||
"""
|
|
||||||
await self.booker.booker(current_time)
|
|
||||||
|
|
||||||
def quit(self) -> None:
|
|
||||||
"""
|
|
||||||
Clean up resources and exit the script.
|
|
||||||
"""
|
|
||||||
self.booker.quit()
|
|
||||||
@@ -36,20 +36,19 @@ class SessionConfig:
|
|||||||
preferred_sessions.append((day_of_week, start_time, session_name_contains))
|
preferred_sessions.append((day_of_week, start_time, session_name_contains))
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# Log a warning if the file is not found
|
# Log error and exit if the file is not found
|
||||||
logging.warning(f"Configuration file '{config_file}' not found. Falling back to default settings.")
|
logging.error(f"Configuration file '{config_file}' not found. Please create the configuration file.")
|
||||||
|
logging.error("You can copy 'preferred_sessions.json.example' to 'preferred_sessions.json' as a template.")
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
# Log a warning if the file is not a valid JSON
|
# Log error and exit if the file is not a valid JSON
|
||||||
logging.warning(f"Failed to decode JSON from file '{config_file}'. Falling back to default settings.")
|
logging.error(f"Failed to decode JSON from file '{config_file}'. Please check the file format.")
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
# Fallback to default hardcoded sessions if no valid sessions were loaded
|
# Return empty list if no valid sessions were loaded
|
||||||
if not preferred_sessions:
|
if not preferred_sessions:
|
||||||
preferred_sessions = [
|
return []
|
||||||
(2, "18:30", "CONDITIONING"), # Wednesday 18:30 CONDITIONING
|
|
||||||
(4, "17:00", "WEIGHTLIFTING"), # Friday 17:00 WEIGHTLIFTING
|
|
||||||
(5, "12:30", "HYROX"), # Saturday 12:30 HYROX
|
|
||||||
]
|
|
||||||
|
|
||||||
return preferred_sessions
|
return preferred_sessions
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import smtplib
|
import smtplib
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import asyncio
|
||||||
from email.message import EmailMessage
|
from email.message import EmailMessage
|
||||||
from telegram import Bot
|
from telegram import Bot
|
||||||
|
from telegram.error import TimedOut, NetworkError
|
||||||
|
|
||||||
|
|
||||||
class SessionNotifier:
|
class SessionNotifier:
|
||||||
@@ -37,6 +39,9 @@ class SessionNotifier:
|
|||||||
# Check environment variable for impossible booking notifications
|
# Check environment variable for impossible booking notifications
|
||||||
self.notify_impossible = os.environ.get("NOTIFY_IMPOSSIBLE_BOOKING", "true").lower() in ("true", "1", "yes")
|
self.notify_impossible = os.environ.get("NOTIFY_IMPOSSIBLE_BOOKING", "true").lower() in ("true", "1", "yes")
|
||||||
|
|
||||||
|
# Get crossfit username from environment
|
||||||
|
self.crossfit_username = os.environ.get("CROSSFIT_USERNAME", "User")
|
||||||
|
|
||||||
def send_email_notification(self, message):
|
def send_email_notification(self, message):
|
||||||
"""
|
"""
|
||||||
Send an email notification with the given message.
|
Send an email notification with the given message.
|
||||||
@@ -75,17 +80,34 @@ class SessionNotifier:
|
|||||||
logging.error(f"Failed to send email: {str(e)}")
|
logging.error(f"Failed to send email: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def send_telegram_notification(self, message):
|
async def send_telegram_notification(self, message, max_retries=3):
|
||||||
"""
|
"""
|
||||||
Send a Telegram notification with the given message.
|
Send a Telegram notification with the given message.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
message (str): The message content to be sent in the Telegram chat
|
message (str): The message content to be sent in the Telegram chat
|
||||||
|
max_retries (int): Maximum number of retry attempts (default: 3)
|
||||||
"""
|
"""
|
||||||
# Create a Bot instance with the provided token
|
# Create a Bot instance with the provided token
|
||||||
bot = Bot(token=self.telegram_credentials["token"])
|
bot = Bot(token=self.telegram_credentials["token"])
|
||||||
|
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
# Send the message to the specified chat ID and await the result
|
# Send the message to the specified chat ID and await the result
|
||||||
await bot.send_message(chat_id=self.telegram_credentials["chat_id"], text=message)
|
await bot.send_message(chat_id=self.telegram_credentials["chat_id"], text=message)
|
||||||
|
logging.debug("Telegram notification sent successfully")
|
||||||
|
return
|
||||||
|
except (TimedOut, NetworkError) as e:
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
wait_time = 2 ** attempt # Exponential backoff: 1s, 2s, 4s
|
||||||
|
logging.warning(f"Telegram notification failed (attempt {attempt + 1}/{max_retries}): {str(e)}. Retrying in {wait_time} seconds...")
|
||||||
|
await asyncio.sleep(wait_time)
|
||||||
|
else:
|
||||||
|
logging.error(f"Failed to send Telegram notification after {max_retries} attempts: {str(e)}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Unexpected error sending Telegram notification: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
async def notify_session_booking(self, session_details):
|
async def notify_session_booking(self, session_details):
|
||||||
"""
|
"""
|
||||||
@@ -95,8 +117,8 @@ class SessionNotifier:
|
|||||||
session_details (str): Details about the booked session
|
session_details (str): Details about the booked session
|
||||||
"""
|
"""
|
||||||
# Create messages for both email and Telegram
|
# Create messages for both email and Telegram
|
||||||
email_message = f"Session booked: {session_details}"
|
email_message = f"Session booked for {self.crossfit_username}: {session_details}"
|
||||||
telegram_message = f"Session booked: {session_details}"
|
telegram_message = f"Session booked for {self.crossfit_username}: {session_details}"
|
||||||
|
|
||||||
# Send notifications through enabled channels
|
# Send notifications through enabled channels
|
||||||
if self.enable_email:
|
if self.enable_email:
|
||||||
@@ -114,8 +136,8 @@ class SessionNotifier:
|
|||||||
days_until (int): Number of days until the session
|
days_until (int): Number of days until the session
|
||||||
"""
|
"""
|
||||||
# Create messages for both email and Telegram
|
# Create messages for both email and Telegram
|
||||||
email_message = f"Session available soon: {session_details} (in {days_until} days)"
|
email_message = f"Session available soon for {self.crossfit_username}: {session_details} (in {days_until} days)"
|
||||||
telegram_message = f"Session available soon: {session_details} (in {days_until} days)"
|
telegram_message = f"Session available soon for {self.crossfit_username}: {session_details} (in {days_until} days)"
|
||||||
|
|
||||||
# Send notifications through enabled channels
|
# Send notifications through enabled channels
|
||||||
if self.enable_email:
|
if self.enable_email:
|
||||||
@@ -145,8 +167,8 @@ class SessionNotifier:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Create messages for both email and Telegram
|
# Create messages for both email and Telegram
|
||||||
email_message = f"Failed to book session: {session_details}"
|
email_message = f"Failed to book session for {self.crossfit_username}: {session_details}"
|
||||||
telegram_message = f"Failed to book session: {session_details}"
|
telegram_message = f"Failed to book session for {self.crossfit_username}: {session_details}"
|
||||||
|
|
||||||
# Send notifications through enabled channels
|
# Send notifications through enabled channels
|
||||||
if self.enable_email:
|
if self.enable_email:
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script to verify booking window functionality.
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import pytz
|
|
||||||
|
|
||||||
# Add the parent directory to the path
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
|
|
||||||
from src.crossfit_booker import CrossFitBooker
|
|
||||||
from src.booker import Booker
|
|
||||||
from src.auth import AuthHandler
|
|
||||||
|
|
||||||
# Set up logging
|
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
||||||
|
|
||||||
# Mock the login method to avoid actual authentication
|
|
||||||
def mock_login(self) -> bool:
|
|
||||||
self.auth_token = "mock_token"
|
|
||||||
self.user_id = "12345"
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Test the booking window functionality
|
|
||||||
def test_booking_window():
|
|
||||||
"""Test the booking window functionality."""
|
|
||||||
# Create a booker instance
|
|
||||||
booker = CrossFitBooker()
|
|
||||||
|
|
||||||
# Replace the login method with our mock
|
|
||||||
original_login = AuthHandler.login
|
|
||||||
AuthHandler.login = mock_login
|
|
||||||
|
|
||||||
# Set up timezone and target time
|
|
||||||
tz = pytz.timezone("Europe/Paris")
|
|
||||||
current_time = datetime.now(tz)
|
|
||||||
|
|
||||||
# Get the target time from the environment variable or use default
|
|
||||||
target_time_str = os.environ.get("TARGET_RESERVATION_TIME", "20:01")
|
|
||||||
target_hour, target_minute = map(int, target_time_str.split(":"))
|
|
||||||
target_time = current_time.replace(hour=target_hour, minute=target_minute, second=0, microsecond=0)
|
|
||||||
|
|
||||||
# Calculate booking window end
|
|
||||||
booking_window_end = target_time + timedelta(minutes=10)
|
|
||||||
|
|
||||||
# Display current time and booking window
|
|
||||||
logging.info(f"Current time: {current_time}")
|
|
||||||
logging.info(f"Target booking time: {target_time}")
|
|
||||||
logging.info(f"Booking window end: {booking_window_end}")
|
|
||||||
|
|
||||||
# Check if we're in the booking window
|
|
||||||
if target_time <= current_time <= booking_window_end:
|
|
||||||
logging.info("We are within the booking window!")
|
|
||||||
else:
|
|
||||||
logging.info("We are outside the booking window.")
|
|
||||||
time_diff = (target_time - current_time).total_seconds()
|
|
||||||
logging.info(f"Next booking window starts in: {time_diff//60} minutes and {time_diff%60:.0f} seconds")
|
|
||||||
|
|
||||||
# Restore the original login method
|
|
||||||
AuthHandler.login = original_login
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
test_booking_window()
|
|
||||||
@@ -12,7 +12,9 @@ from unittest.mock import patch, Mock
|
|||||||
# Add the parent directory to the path
|
# Add the parent directory to the path
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from src.crossfit_booker import CrossFitBooker
|
from src.auth import AuthHandler
|
||||||
|
from src.booker import Booker
|
||||||
|
from src.session_notifier import SessionNotifier
|
||||||
|
|
||||||
class TestCrossFitBookerAuthHeaders:
|
class TestCrossFitBookerAuthHeaders:
|
||||||
"""Test cases for get_auth_headers method"""
|
"""Test cases for get_auth_headers method"""
|
||||||
@@ -23,8 +25,8 @@ class TestCrossFitBookerAuthHeaders:
|
|||||||
'CROSSFIT_USERNAME': 'test_user',
|
'CROSSFIT_USERNAME': 'test_user',
|
||||||
'CROSSFIT_PASSWORD': 'test_pass'
|
'CROSSFIT_PASSWORD': 'test_pass'
|
||||||
}):
|
}):
|
||||||
booker = CrossFitBooker()
|
auth_handler = AuthHandler('test_user', 'test_pass')
|
||||||
headers = booker.get_auth_headers()
|
headers = auth_handler.get_auth_headers()
|
||||||
assert "Authorization" not in headers
|
assert "Authorization" not in headers
|
||||||
assert headers["User-Agent"] == "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:140.0) Gecko/20100101 Firefox/140.0"
|
assert headers["User-Agent"] == "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:140.0) Gecko/20100101 Firefox/140.0"
|
||||||
|
|
||||||
@@ -34,13 +36,13 @@ class TestCrossFitBookerAuthHeaders:
|
|||||||
'CROSSFIT_USERNAME': 'test_user',
|
'CROSSFIT_USERNAME': 'test_user',
|
||||||
'CROSSFIT_PASSWORD': 'test_pass'
|
'CROSSFIT_PASSWORD': 'test_pass'
|
||||||
}):
|
}):
|
||||||
booker = CrossFitBooker()
|
auth_handler = AuthHandler('test_user', 'test_pass')
|
||||||
booker.auth_handler.auth_token = "test_token_123"
|
auth_handler.auth_token = "test_token_123"
|
||||||
headers = booker.get_auth_headers()
|
headers = auth_handler.get_auth_headers()
|
||||||
assert headers["Authorization"] == "Bearer test_token_123"
|
assert headers["Authorization"] == "Bearer test_token_123"
|
||||||
|
|
||||||
class TestCrossFitBookerLogin:
|
class TestAuthHandlerLogin:
|
||||||
"""Test cases for login method"""
|
"""Test cases for AuthHandler login method"""
|
||||||
|
|
||||||
@patch('requests.Session.post')
|
@patch('requests.Session.post')
|
||||||
def test_login_success(self, mock_post):
|
def test_login_success(self, mock_post):
|
||||||
@@ -69,12 +71,12 @@ class TestCrossFitBookerLogin:
|
|||||||
'CROSSFIT_USERNAME': 'test_user',
|
'CROSSFIT_USERNAME': 'test_user',
|
||||||
'CROSSFIT_PASSWORD': 'test_pass'
|
'CROSSFIT_PASSWORD': 'test_pass'
|
||||||
}):
|
}):
|
||||||
booker = CrossFitBooker()
|
auth_handler = AuthHandler('test_user', 'test_pass')
|
||||||
result = booker.login()
|
result = auth_handler.login()
|
||||||
|
|
||||||
assert result is True
|
assert result is True
|
||||||
assert booker.auth_handler.user_id == "12345"
|
assert auth_handler.user_id == "12345"
|
||||||
assert booker.auth_handler.auth_token == "test_bearer_token"
|
assert auth_handler.auth_token == "test_bearer_token"
|
||||||
|
|
||||||
@patch('requests.Session.post')
|
@patch('requests.Session.post')
|
||||||
def test_login_first_step_failure(self, mock_post):
|
def test_login_first_step_failure(self, mock_post):
|
||||||
@@ -90,12 +92,12 @@ class TestCrossFitBookerLogin:
|
|||||||
'CROSSFIT_USERNAME': 'test_user',
|
'CROSSFIT_USERNAME': 'test_user',
|
||||||
'CROSSFIT_PASSWORD': 'test_pass'
|
'CROSSFIT_PASSWORD': 'test_pass'
|
||||||
}):
|
}):
|
||||||
booker = CrossFitBooker()
|
auth_handler = AuthHandler('test_user', 'test_pass')
|
||||||
result = booker.login()
|
result = auth_handler.login()
|
||||||
|
|
||||||
assert result is False
|
assert result is False
|
||||||
assert booker.auth_handler.user_id is None
|
assert auth_handler.user_id is None
|
||||||
assert booker.auth_handler.auth_token is None
|
assert auth_handler.auth_token is None
|
||||||
|
|
||||||
@patch('requests.Session.post')
|
@patch('requests.Session.post')
|
||||||
def test_login_second_step_failure(self, mock_post):
|
def test_login_second_step_failure(self, mock_post):
|
||||||
@@ -122,8 +124,8 @@ class TestCrossFitBookerLogin:
|
|||||||
'CROSSFIT_USERNAME': 'test_user',
|
'CROSSFIT_USERNAME': 'test_user',
|
||||||
'CROSSFIT_PASSWORD': 'test_pass'
|
'CROSSFIT_PASSWORD': 'test_pass'
|
||||||
}):
|
}):
|
||||||
booker = CrossFitBooker()
|
auth_handler = AuthHandler('test_user', 'test_pass')
|
||||||
result = booker.login()
|
result = auth_handler.login()
|
||||||
|
|
||||||
assert result is False
|
assert result is False
|
||||||
|
|
||||||
@@ -140,8 +142,8 @@ class TestCrossFitBookerLogin:
|
|||||||
'CROSSFIT_USERNAME': 'test_user',
|
'CROSSFIT_USERNAME': 'test_user',
|
||||||
'CROSSFIT_PASSWORD': 'test_pass'
|
'CROSSFIT_PASSWORD': 'test_pass'
|
||||||
}):
|
}):
|
||||||
booker = CrossFitBooker()
|
auth_handler = AuthHandler('test_user', 'test_pass')
|
||||||
result = booker.login()
|
result = auth_handler.login()
|
||||||
|
|
||||||
assert result is False
|
assert result is False
|
||||||
|
|
||||||
@@ -154,8 +156,8 @@ class TestCrossFitBookerLogin:
|
|||||||
'CROSSFIT_USERNAME': 'test_user',
|
'CROSSFIT_USERNAME': 'test_user',
|
||||||
'CROSSFIT_PASSWORD': 'test_pass'
|
'CROSSFIT_PASSWORD': 'test_pass'
|
||||||
}):
|
}):
|
||||||
booker = CrossFitBooker()
|
auth_handler = AuthHandler('test_user', 'test_pass')
|
||||||
result = booker.login()
|
result = auth_handler.login()
|
||||||
|
|
||||||
assert result is False
|
assert result is False
|
||||||
|
|
||||||
|
|||||||
@@ -1,232 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Comprehensive unit tests for the CrossFitBooker class in crossfit_booker.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from unittest.mock import Mock, patch
|
|
||||||
from datetime import date
|
|
||||||
import requests
|
|
||||||
|
|
||||||
# Add the parent directory to the path to import crossfit_booker
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
from src.crossfit_booker import CrossFitBooker
|
|
||||||
|
|
||||||
class TestCrossFitBookerInit:
|
|
||||||
"""Test cases for CrossFitBooker initialization"""
|
|
||||||
|
|
||||||
def test_init_success(self):
|
|
||||||
"""Test successful initialization with all required env vars"""
|
|
||||||
with patch.dict(os.environ, {
|
|
||||||
'CROSSFIT_USERNAME': 'test_user',
|
|
||||||
'CROSSFIT_PASSWORD': 'test_pass',
|
|
||||||
'EMAIL_FROM': 'from@test.com',
|
|
||||||
'EMAIL_TO': 'to@test.com',
|
|
||||||
'EMAIL_PASSWORD': 'email_pass',
|
|
||||||
'TELEGRAM_TOKEN': 'telegram_token',
|
|
||||||
'TELEGRAM_CHAT_ID': '12345'
|
|
||||||
}):
|
|
||||||
booker = CrossFitBooker()
|
|
||||||
assert booker.auth_handler.auth_token is None
|
|
||||||
assert booker.auth_handler.user_id is None
|
|
||||||
assert booker.session is not None
|
|
||||||
assert booker.notifier is not None
|
|
||||||
|
|
||||||
def test_init_missing_credentials(self):
|
|
||||||
"""Test initialization fails with missing credentials"""
|
|
||||||
with patch.dict(os.environ, {}, clear=True):
|
|
||||||
try:
|
|
||||||
CrossFitBooker()
|
|
||||||
except ValueError as e:
|
|
||||||
assert str(e) == "Missing environment variables"
|
|
||||||
|
|
||||||
def test_init_partial_credentials(self):
|
|
||||||
"""Test initialization fails with partial credentials"""
|
|
||||||
with patch.dict(os.environ, {
|
|
||||||
'CROSSFIT_USERNAME': 'test_user'
|
|
||||||
# Missing PASSWORD
|
|
||||||
}, clear=True):
|
|
||||||
try:
|
|
||||||
CrossFitBooker()
|
|
||||||
except ValueError as e:
|
|
||||||
assert str(e) == "Missing environment variables"
|
|
||||||
|
|
||||||
class TestCrossFitBookerAuthHeaders:
|
|
||||||
"""Test cases for get_auth_headers method"""
|
|
||||||
|
|
||||||
def test_get_auth_headers_without_token(self):
|
|
||||||
"""Test headers without auth token"""
|
|
||||||
with patch.dict(os.environ, {
|
|
||||||
'CROSSFIT_USERNAME': 'test_user',
|
|
||||||
'CROSSFIT_PASSWORD': 'test_pass'
|
|
||||||
}):
|
|
||||||
booker = CrossFitBooker()
|
|
||||||
headers = booker.get_auth_headers()
|
|
||||||
assert "Authorization" not in headers
|
|
||||||
assert headers["User-Agent"] == "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:140.0) Gecko/20100101 Firefox/140.0"
|
|
||||||
|
|
||||||
def test_get_auth_headers_with_token(self):
|
|
||||||
"""Test headers with auth token"""
|
|
||||||
with patch.dict(os.environ, {
|
|
||||||
'CROSSFIT_USERNAME': 'test_user',
|
|
||||||
'CROSSFIT_PASSWORD': 'test_pass'
|
|
||||||
}):
|
|
||||||
booker = CrossFitBooker()
|
|
||||||
booker.auth_handler.auth_token = "test_token_123"
|
|
||||||
headers = booker.get_auth_headers()
|
|
||||||
assert headers["Authorization"] == "Bearer test_token_123"
|
|
||||||
|
|
||||||
class TestCrossFitBookerLogin:
|
|
||||||
"""Test cases for login method"""
|
|
||||||
|
|
||||||
@patch('requests.Session.post')
|
|
||||||
def test_login_success(self, mock_post):
|
|
||||||
"""Test successful login flow"""
|
|
||||||
# Mock first login response
|
|
||||||
mock_response1 = Mock()
|
|
||||||
mock_response1.ok = True
|
|
||||||
mock_response1.json.return_value = {
|
|
||||||
"data": {
|
|
||||||
"user": {
|
|
||||||
"id_user": "12345"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Mock second login response
|
|
||||||
mock_response2 = Mock()
|
|
||||||
mock_response2.ok = True
|
|
||||||
mock_response2.json.return_value = {
|
|
||||||
"token": "test_bearer_token"
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_post.side_effect = [mock_response1, mock_response2]
|
|
||||||
|
|
||||||
with patch.dict(os.environ, {
|
|
||||||
'CROSSFIT_USERNAME': 'test_user',
|
|
||||||
'CROSSFIT_PASSWORD': 'test_pass'
|
|
||||||
}):
|
|
||||||
booker = CrossFitBooker()
|
|
||||||
result = booker.login()
|
|
||||||
|
|
||||||
assert result is True
|
|
||||||
assert booker.auth_handler.user_id == "12345"
|
|
||||||
assert booker.auth_handler.auth_token == "test_bearer_token"
|
|
||||||
|
|
||||||
@patch('requests.Session.post')
|
|
||||||
def test_login_first_step_failure(self, mock_post):
|
|
||||||
"""Test login failure on first step"""
|
|
||||||
mock_response = Mock()
|
|
||||||
mock_response.ok = False
|
|
||||||
mock_response.status_code = 400
|
|
||||||
mock_response.text = "Bad Request"
|
|
||||||
|
|
||||||
mock_post.return_value = mock_response
|
|
||||||
|
|
||||||
with patch.dict(os.environ, {
|
|
||||||
'CROSSFIT_USERNAME': 'test_user',
|
|
||||||
'CROSSFIT_PASSWORD': 'test_pass'
|
|
||||||
}):
|
|
||||||
booker = CrossFitBooker()
|
|
||||||
result = booker.login()
|
|
||||||
|
|
||||||
assert result is False
|
|
||||||
assert booker.auth_handler.user_id is None
|
|
||||||
assert booker.auth_handler.auth_token is None
|
|
||||||
|
|
||||||
@patch('requests.Session.post')
|
|
||||||
def test_login_json_parsing_error(self, mock_post):
|
|
||||||
"""Test login with JSON parsing error"""
|
|
||||||
mock_response = Mock()
|
|
||||||
mock_response.ok = True
|
|
||||||
mock_response.json.side_effect = ValueError("Invalid JSON")
|
|
||||||
|
|
||||||
mock_post.return_value = mock_response
|
|
||||||
|
|
||||||
with patch.dict(os.environ, {
|
|
||||||
'CROSSFIT_USERNAME': 'test_user',
|
|
||||||
'CROSSFIT_PASSWORD': 'test_pass'
|
|
||||||
}):
|
|
||||||
booker = CrossFitBooker()
|
|
||||||
result = booker.login()
|
|
||||||
|
|
||||||
assert result is False
|
|
||||||
|
|
||||||
@patch('requests.Session.post')
|
|
||||||
def test_login_request_exception(self, mock_post):
|
|
||||||
"""Test login with request exception"""
|
|
||||||
mock_post.side_effect = requests.exceptions.ConnectionError("Network error")
|
|
||||||
|
|
||||||
with patch.dict(os.environ, {
|
|
||||||
'CROSSFIT_USERNAME': 'test_user',
|
|
||||||
'CROSSFIT_PASSWORD': 'test_pass'
|
|
||||||
}):
|
|
||||||
booker = CrossFitBooker()
|
|
||||||
result = booker.login()
|
|
||||||
|
|
||||||
assert result is False
|
|
||||||
|
|
||||||
class TestCrossFitBookerGetAvailableSessions:
|
|
||||||
"""Test cases for get_available_sessions method"""
|
|
||||||
|
|
||||||
def test_get_available_sessions_no_auth(self):
|
|
||||||
"""Test get_available_sessions without authentication"""
|
|
||||||
with patch.dict(os.environ, {
|
|
||||||
'CROSSFIT_USERNAME': 'test_user',
|
|
||||||
'CROSSFIT_PASSWORD': 'test_pass'
|
|
||||||
}):
|
|
||||||
booker = CrossFitBooker()
|
|
||||||
result = booker.get_available_sessions(date(2025, 7, 24), date(2025, 7, 25))
|
|
||||||
assert result is None
|
|
||||||
|
|
||||||
@patch('requests.Session.post')
|
|
||||||
def test_get_available_sessions_success(self, mock_post):
|
|
||||||
"""Test successful get_available_sessions"""
|
|
||||||
mock_response = Mock()
|
|
||||||
mock_response.status_code = 200
|
|
||||||
mock_response.json.return_value = {
|
|
||||||
"success": True,
|
|
||||||
"data": {
|
|
||||||
"activities_calendar": [
|
|
||||||
{"id": "1", "name": "Test Session"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_post.return_value = mock_response
|
|
||||||
|
|
||||||
with patch.dict(os.environ, {
|
|
||||||
'CROSSFIT_USERNAME': 'test_user',
|
|
||||||
'CROSSFIT_PASSWORD': 'test_pass'
|
|
||||||
}):
|
|
||||||
booker = CrossFitBooker()
|
|
||||||
booker.auth_handler.auth_token = "test_token"
|
|
||||||
booker.auth_handler.user_id = "12345"
|
|
||||||
|
|
||||||
result = booker.get_available_sessions(date(2025, 7, 24), date(2025, 7, 25))
|
|
||||||
|
|
||||||
assert result is not None
|
|
||||||
assert result["success"] is True
|
|
||||||
|
|
||||||
@patch('requests.Session.post')
|
|
||||||
def test_get_available_sessions_failure(self, mock_post):
|
|
||||||
"""Test get_available_sessions with API failure"""
|
|
||||||
mock_response = Mock()
|
|
||||||
mock_response.status_code = 400
|
|
||||||
mock_response.text = "Bad Request"
|
|
||||||
|
|
||||||
mock_post.return_value = mock_response
|
|
||||||
|
|
||||||
with patch.dict(os.environ, {
|
|
||||||
'CROSSFIT_USERNAME': 'test_user',
|
|
||||||
'CROSSFIT_PASSWORD': 'test_pass'
|
|
||||||
}):
|
|
||||||
booker = CrossFitBooker()
|
|
||||||
booker.auth_handler.auth_token = "test_token"
|
|
||||||
booker.auth_handler.user_id = "12345"
|
|
||||||
|
|
||||||
result = booker.get_available_sessions(date(2025, 7, 24), date(2025, 7, 25))
|
|
||||||
|
|
||||||
assert result is None
|
|
||||||
@@ -13,9 +13,10 @@ import pytz
|
|||||||
# Add the parent directory to the path
|
# Add the parent directory to the path
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from src.crossfit_booker import CrossFitBooker
|
|
||||||
from src.session_manager import SessionManager
|
|
||||||
from src.auth import AuthHandler
|
from src.auth import AuthHandler
|
||||||
|
from src.booker import Booker
|
||||||
|
from src.session_notifier import SessionNotifier
|
||||||
|
from src.session_manager import SessionManager
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -75,11 +76,12 @@ class TestCrossFitBookerGetAvailableSessions:
|
|||||||
'CROSSFIT_USERNAME': 'test_user',
|
'CROSSFIT_USERNAME': 'test_user',
|
||||||
'CROSSFIT_PASSWORD': 'test_pass'
|
'CROSSFIT_PASSWORD': 'test_pass'
|
||||||
}):
|
}):
|
||||||
booker = CrossFitBooker()
|
auth_handler = AuthHandler('test_user', 'test_pass')
|
||||||
booker.auth_handler.auth_token = "test_token"
|
session_manager = SessionManager(auth_handler)
|
||||||
booker.auth_handler.user_id = "12345"
|
auth_handler.auth_token = "test_token"
|
||||||
|
auth_handler.user_id = "12345"
|
||||||
|
|
||||||
result = booker.get_available_sessions(date(2025, 7, 24), date(2025, 7, 25))
|
result = session_manager.get_available_sessions(date(2025, 7, 24), date(2025, 7, 25))
|
||||||
|
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
@@ -198,129 +200,6 @@ class TestCrossFitBookerIsSessionBookable:
|
|||||||
result = session_manager.is_session_bookable(session, current_time)
|
result = session_manager.is_session_bookable(session, current_time)
|
||||||
assert result is False
|
assert result is False
|
||||||
|
|
||||||
class TestCrossFitBookerExcuteCycle:
|
|
||||||
"""Test cases for execute_cycle method"""
|
|
||||||
|
|
||||||
@patch('src.crossfit_booker.CrossFitBooker.get_available_sessions')
|
|
||||||
@patch('src.crossfit_booker.CrossFitBooker.is_session_bookable')
|
|
||||||
@patch('src.crossfit_booker.CrossFitBooker.matches_preferred_session')
|
|
||||||
@patch('src.crossfit_booker.CrossFitBooker.book_session')
|
|
||||||
async def test_run_booking_cycle_no_sessions(self, mock_book_session, mock_matches_preferred, mock_is_bookable, mock_get_sessions):
|
|
||||||
"""Test run_booking_cycle with no available sessions"""
|
|
||||||
mock_get_sessions.return_value = {"success": False}
|
|
||||||
booker = CrossFitBooker()
|
|
||||||
# Mock the auth_token and user_id to avoid authentication errors
|
|
||||||
booker.auth_handler.auth_token = "test_token"
|
|
||||||
booker.auth_handler.user_id = "12345"
|
|
||||||
# Mock the booker method to use our mocked methods
|
|
||||||
with patch.object(booker.booker, 'get_available_sessions', mock_get_sessions):
|
|
||||||
await booker.run_booking_cycle(datetime.now(pytz.timezone("Europe/Paris")))
|
|
||||||
mock_get_sessions.assert_called_once()
|
|
||||||
mock_book_session.assert_not_called()
|
|
||||||
|
|
||||||
@patch('src.crossfit_booker.CrossFitBooker.get_available_sessions')
|
|
||||||
@patch('src.crossfit_booker.CrossFitBooker.is_session_bookable')
|
|
||||||
@patch('src.crossfit_booker.CrossFitBooker.matches_preferred_session')
|
|
||||||
@patch('src.crossfit_booker.CrossFitBooker.book_session')
|
|
||||||
async def test_run_booking_cycle_with_sessions(self, mock_book_session, mock_matches_preferred, mock_is_bookable, mock_get_sessions):
|
|
||||||
"""Test run_booking_cycle with available sessions"""
|
|
||||||
# Use current date for the session to ensure it falls within 0-2 day window
|
|
||||||
current_time = datetime.now(pytz.timezone("Europe/Paris"))
|
|
||||||
session_date = current_time.date()
|
|
||||||
|
|
||||||
mock_get_sessions.return_value = {
|
|
||||||
"success": True,
|
|
||||||
"data": {
|
|
||||||
"activities_calendar": [
|
|
||||||
{
|
|
||||||
"id_activity_calendar": "1",
|
|
||||||
"name_activity": "CONDITIONING",
|
|
||||||
"start_timestamp": session_date.strftime("%Y-%m-%d") + " 18:30:00",
|
|
||||||
"user_info": {"can_join": True}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_is_bookable.return_value = True
|
|
||||||
mock_matches_preferred.return_value = True
|
|
||||||
mock_book_session.return_value = True
|
|
||||||
|
|
||||||
booker = CrossFitBooker()
|
|
||||||
# Mock the auth_token and user_id to avoid authentication errors
|
|
||||||
booker.auth_handler.auth_token = "test_token"
|
|
||||||
booker.auth_handler.user_id = "12345"
|
|
||||||
# Mock the booker method to use our mocked methods
|
|
||||||
with patch.object(booker.booker, 'get_available_sessions', mock_get_sessions):
|
|
||||||
with patch.object(booker.booker, 'is_session_bookable', mock_is_bookable):
|
|
||||||
with patch.object(booker.booker, 'matches_preferred_session', mock_matches_preferred):
|
|
||||||
with patch.object(booker.booker, 'book_session', mock_book_session):
|
|
||||||
await booker.run_booking_cycle(current_time)
|
|
||||||
|
|
||||||
mock_get_sessions.assert_called_once()
|
|
||||||
mock_is_bookable.assert_called_once()
|
|
||||||
mock_matches_preferred.assert_called_once()
|
|
||||||
mock_book_session.assert_called_once()
|
|
||||||
assert mock_book_session.call_count == 1
|
|
||||||
|
|
||||||
class TestCrossFitBookerRun:
|
|
||||||
"""Test cases for run method"""
|
|
||||||
|
|
||||||
def test_run_auth_failure(self):
|
|
||||||
"""Test run with authentication failure"""
|
|
||||||
with patch('src.crossfit_booker.CrossFitBooker.login', return_value=False) as mock_login:
|
|
||||||
booker = CrossFitBooker()
|
|
||||||
# Test the authentication failure path through the booker
|
|
||||||
result = booker.login()
|
|
||||||
assert result is False
|
|
||||||
mock_login.assert_called_once()
|
|
||||||
|
|
||||||
def test_run_booking_outside_window(self):
|
|
||||||
"""Test run with booking outside window"""
|
|
||||||
with patch('src.booker.Booker.run') as mock_run:
|
|
||||||
with patch('datetime.datetime') as mock_datetime:
|
|
||||||
with patch('time.sleep') as mock_sleep:
|
|
||||||
# Create a time outside the booking window (19:00)
|
|
||||||
tz = pytz.timezone("Europe/Paris")
|
|
||||||
mock_now = datetime(2025, 7, 25, 19, 0, tzinfo=tz)
|
|
||||||
mock_datetime.now.return_value = mock_now
|
|
||||||
|
|
||||||
# Make sleep return immediately to allow one iteration, then break
|
|
||||||
call_count = 0
|
|
||||||
def sleep_side_effect(seconds):
|
|
||||||
nonlocal call_count
|
|
||||||
call_count += 1
|
|
||||||
if call_count >= 1:
|
|
||||||
# Break the loop after first sleep
|
|
||||||
raise KeyboardInterrupt("Test complete")
|
|
||||||
return None
|
|
||||||
|
|
||||||
mock_sleep.side_effect = sleep_side_effect
|
|
||||||
|
|
||||||
booker = CrossFitBooker()
|
|
||||||
|
|
||||||
# Test the booking window logic directly
|
|
||||||
target_hour, target_minute = map(int, "20:01".split(":"))
|
|
||||||
target_time = datetime.now(tz).replace(hour=target_hour, minute=target_minute, second=0, microsecond=0)
|
|
||||||
booking_window_end = target_time + timedelta(minutes=10)
|
|
||||||
|
|
||||||
# Current time is outside the booking window
|
|
||||||
assert not (target_time <= mock_now <= booking_window_end)
|
|
||||||
|
|
||||||
# Run the booker to trigger the login
|
|
||||||
booker.run()
|
|
||||||
|
|
||||||
# Verify run was called
|
|
||||||
mock_run.assert_called_once()
|
|
||||||
|
|
||||||
class TestCrossFitBookerQuit:
|
|
||||||
"""Test cases for quit method"""
|
|
||||||
|
|
||||||
def test_quit(self):
|
|
||||||
"""Test quit method"""
|
|
||||||
booker = CrossFitBooker()
|
|
||||||
with pytest.raises(SystemExit) as excinfo:
|
|
||||||
booker.quit()
|
|
||||||
assert excinfo.value.code == 0
|
|
||||||
|
|
||||||
class TestCrossFitBookerMatchesPreferredSession:
|
class TestCrossFitBookerMatchesPreferredSession:
|
||||||
"""Test cases for matches_preferred_session method"""
|
"""Test cases for matches_preferred_session method"""
|
||||||
|
|||||||
@@ -37,18 +37,17 @@ class TestSessionConfig:
|
|||||||
"""Test behavior when the config file is not found"""
|
"""Test behavior when the config file is not found"""
|
||||||
# Mock the open function to raise FileNotFoundError
|
# Mock the open function to raise FileNotFoundError
|
||||||
with patch('builtins.open', side_effect=FileNotFoundError):
|
with patch('builtins.open', side_effect=FileNotFoundError):
|
||||||
with patch('logging.warning') as mock_warning:
|
with patch('logging.error') as mock_error:
|
||||||
sessions = SessionConfig.load_preferred_sessions()
|
with pytest.raises(SystemExit) as excinfo:
|
||||||
|
SessionConfig.load_preferred_sessions()
|
||||||
|
|
||||||
# Verify warning was logged
|
# Verify error was logged
|
||||||
mock_warning.assert_called_once()
|
assert mock_error.call_count == 2
|
||||||
assert "not found" in mock_warning.call_args[0][0]
|
assert "not found" in mock_error.call_args_list[0][0][0]
|
||||||
|
assert "example" in mock_error.call_args_list[1][0][0]
|
||||||
|
|
||||||
# Verify default sessions are returned
|
# Verify SystemExit was raised with exit code 1
|
||||||
assert len(sessions) == 3
|
assert excinfo.value.code == 1
|
||||||
assert sessions[0] == (2, "18:30", "CONDITIONING")
|
|
||||||
assert sessions[1] == (4, "17:00", "WEIGHTLIFTING")
|
|
||||||
assert sessions[2] == (5, "12:30", "HYROX")
|
|
||||||
|
|
||||||
def test_load_preferred_sessions_invalid_json(self):
|
def test_load_preferred_sessions_invalid_json(self):
|
||||||
"""Test behavior when the config file contains invalid JSON"""
|
"""Test behavior when the config file contains invalid JSON"""
|
||||||
@@ -57,18 +56,16 @@ class TestSessionConfig:
|
|||||||
|
|
||||||
# Mock the open function to return invalid JSON
|
# Mock the open function to return invalid JSON
|
||||||
with patch('builtins.open', mock_open(read_data=invalid_json)):
|
with patch('builtins.open', mock_open(read_data=invalid_json)):
|
||||||
with patch('logging.warning') as mock_warning:
|
with patch('logging.error') as mock_error:
|
||||||
sessions = SessionConfig.load_preferred_sessions()
|
with pytest.raises(SystemExit) as excinfo:
|
||||||
|
SessionConfig.load_preferred_sessions()
|
||||||
|
|
||||||
# Verify warning was logged
|
# Verify error was logged
|
||||||
mock_warning.assert_called_once()
|
mock_error.assert_called_once()
|
||||||
assert "decode" in mock_warning.call_args[0][0]
|
assert "decode" in mock_error.call_args[0][0]
|
||||||
|
|
||||||
# Verify default sessions are returned
|
# Verify SystemExit was raised with exit code 1
|
||||||
assert len(sessions) == 3
|
assert excinfo.value.code == 1
|
||||||
assert sessions[0] == (2, "18:30", "CONDITIONING")
|
|
||||||
assert sessions[1] == (4, "17:00", "WEIGHTLIFTING")
|
|
||||||
assert sessions[2] == (5, "12:30", "HYROX")
|
|
||||||
|
|
||||||
def test_load_preferred_sessions_empty_file(self):
|
def test_load_preferred_sessions_empty_file(self):
|
||||||
"""Test behavior when the config file is empty"""
|
"""Test behavior when the config file is empty"""
|
||||||
@@ -79,11 +76,8 @@ class TestSessionConfig:
|
|||||||
with patch('builtins.open', mock_open(read_data=empty_json)):
|
with patch('builtins.open', mock_open(read_data=empty_json)):
|
||||||
sessions = SessionConfig.load_preferred_sessions()
|
sessions = SessionConfig.load_preferred_sessions()
|
||||||
|
|
||||||
# Verify default sessions are returned
|
# Verify empty list is returned
|
||||||
assert len(sessions) == 3
|
assert sessions == []
|
||||||
assert sessions[0] == (2, "18:30", "CONDITIONING")
|
|
||||||
assert sessions[1] == (4, "17:00", "WEIGHTLIFTING")
|
|
||||||
assert sessions[2] == (5, "12:30", "HYROX")
|
|
||||||
|
|
||||||
def test_load_preferred_sessions_missing_fields(self):
|
def test_load_preferred_sessions_missing_fields(self):
|
||||||
"""Test behavior when some fields are missing in the JSON data"""
|
"""Test behavior when some fields are missing in the JSON data"""
|
||||||
@@ -111,18 +105,16 @@ class TestSessionConfig:
|
|||||||
|
|
||||||
# Mock the open function to return partial JSON
|
# Mock the open function to return partial JSON
|
||||||
with patch('builtins.open', mock_open(read_data=partial_json)):
|
with patch('builtins.open', mock_open(read_data=partial_json)):
|
||||||
with patch('logging.warning') as mock_warning:
|
with patch('logging.error') as mock_error:
|
||||||
sessions = SessionConfig.load_preferred_sessions()
|
with pytest.raises(SystemExit) as excinfo:
|
||||||
|
SessionConfig.load_preferred_sessions()
|
||||||
|
|
||||||
# Verify warning was logged
|
# Verify error was logged
|
||||||
mock_warning.assert_called_once()
|
mock_error.assert_called_once()
|
||||||
assert "decode" in mock_warning.call_args[0][0]
|
assert "decode" in mock_error.call_args[0][0]
|
||||||
|
|
||||||
# Verify default sessions are returned
|
# Verify SystemExit was raised with exit code 1
|
||||||
assert len(sessions) == 3
|
assert excinfo.value.code == 1
|
||||||
assert sessions[0] == (2, "18:30", "CONDITIONING")
|
|
||||||
assert sessions[1] == (4, "17:00", "WEIGHTLIFTING")
|
|
||||||
assert sessions[2] == (5, "12:30", "HYROX")
|
|
||||||
|
|
||||||
def test_load_preferred_sessions_incorrect_field_types(self):
|
def test_load_preferred_sessions_incorrect_field_types(self):
|
||||||
"""Test behavior when the config file contains JSON with incorrect field types"""
|
"""Test behavior when the config file contains JSON with incorrect field types"""
|
||||||
|
|||||||
@@ -4,8 +4,15 @@ Script to demonstrate how to execute the book_session method from crossfit_booke
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
import logging
|
import logging
|
||||||
from crossfit_booker import CrossFitBooker
|
from src.auth import AuthHandler
|
||||||
|
from src.booker import Booker
|
||||||
|
from src.session_notifier import SessionNotifier
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
@@ -18,12 +25,40 @@ def main():
|
|||||||
|
|
||||||
session_id = sys.argv[1]
|
session_id = sys.argv[1]
|
||||||
|
|
||||||
# Create an instance of CrossFitBooker
|
# Initialize components
|
||||||
booker = CrossFitBooker()
|
auth_handler = AuthHandler(
|
||||||
|
os.environ.get("CROSSFIT_USERNAME"),
|
||||||
|
os.environ.get("CROSSFIT_PASSWORD")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize notification system (minimal for single session booking)
|
||||||
|
email_credentials = {
|
||||||
|
"from": os.environ.get("EMAIL_FROM"),
|
||||||
|
"to": os.environ.get("EMAIL_TO"),
|
||||||
|
"password": os.environ.get("EMAIL_PASSWORD")
|
||||||
|
}
|
||||||
|
|
||||||
|
telegram_credentials = {
|
||||||
|
"token": os.environ.get("TELEGRAM_TOKEN"),
|
||||||
|
"chat_id": os.environ.get("TELEGRAM_CHAT_ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
enable_email = os.environ.get("ENABLE_EMAIL_NOTIFICATIONS", "true").lower() in ("true", "1", "yes")
|
||||||
|
enable_telegram = os.environ.get("ENABLE_TELEGRAM_NOTIFICATIONS", "true").lower() in ("true", "1", "yes")
|
||||||
|
|
||||||
|
notifier = SessionNotifier(
|
||||||
|
email_credentials,
|
||||||
|
telegram_credentials,
|
||||||
|
enable_email=enable_email,
|
||||||
|
enable_telegram=enable_telegram
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create an instance of Booker
|
||||||
|
booker = Booker(auth_handler, notifier)
|
||||||
|
|
||||||
# Login to authenticate
|
# Login to authenticate
|
||||||
print("Attempting to authenticate...")
|
print("Attempting to authenticate...")
|
||||||
if not booker.login():
|
if not auth_handler.login():
|
||||||
print("Failed to authenticate. Please check your credentials and try again.")
|
print("Failed to authenticate. Please check your credentials and try again.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
print("Authentication successful!")
|
print("Authentication successful!")
|
||||||
|
|||||||
Reference in New Issue
Block a user