initial project setup with README and ignore

This commit is contained in:
2026-04-08 15:13:42 +05:30
commit 2d5688cb35
47 changed files with 7929 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
import httpx
import logging
from datetime import datetime
from typing import List, Dict, Any, Optional
from app.config.rider_preferences import RIDER_PREFERRED_KITCHENS
logger = logging.getLogger(__name__)
async def fetch_active_riders() -> List[Dict[str, Any]]:
"""
Fetch active rider logs from the external API for the current date.
Returns a list of rider log dictionaries.
"""
try:
today_str = datetime.now().strftime("%Y-%m-%d")
url = "https://jupiter.nearle.app/live/api/v2/partners/getriderlogs/"
params = {
"applocationid": 1,
"partnerid": 44,
"fromdate": today_str,
"todate": today_str,
"keyword": ""
}
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(url, params=params)
response.raise_for_status()
data = response.json()
if data and data.get("code") == 200 and data.get("details"):
# Filter riders who are in our preferences list and are 'active' or 'idle' (assuming we want online riders)
# The user's example showed "onduty": 1. We might want to filter by that.
# For now, returning all logs, filtering can happen in assignment logic or here.
# Let's return the raw list as requested, filtering logic will be applied during assignment.
return data.get("details", [])
logger.warning(f"Fetch active riders returned no details: {data}")
return []
except Exception as e:
logger.error(f"Error fetching active riders: {e}", exc_info=True)
return []
async def fetch_created_orders() -> List[Dict[str, Any]]:
"""
Fetch all orders in 'created' state for the current date.
"""
try:
today_str = datetime.now().strftime("%Y-%m-%d")
url = "https://jupiter.nearle.app/live/api/v1/orders/tenant/getorders/"
# Removed pagesize as per user request to fetch all
params = {
"applocationid": 0,
"tenantid": 0,
"locationid": 0,
"status": "created",
"fromdate": today_str,
"todate": today_str,
"keyword": "",
"pageno": 1
# "pagesize" intentionally omitted to fetch all
}
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.get(url, params=params)
response.raise_for_status()
data = response.json()
if data and data.get("code") == 200 and data.get("details"):
return data.get("details", [])
logger.warning(f"Fetch created orders returned no details: {data}")
return []
except Exception as e:
logger.error(f"Error fetching created orders: {e}", exc_info=True)
return []
async def fetch_rider_pricing() -> List[Dict[str, Any]]:
"""
Fetch rider pricing configuration from external API.
"""
try:
url = "https://jupiter.nearle.app/live/api/v1/partners/getriderpricing"
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(url)
response.raise_for_status()
data = response.json()
if data and data.get("code") == 200:
return data.get("details", [])
logger.warning(f"Fetch rider pricing returned no details: {data}")
return []
except Exception as e:
logger.error(f"Error fetching rider pricing: {e}", exc_info=True)
return []

View File

@@ -0,0 +1,78 @@
import os
import pickle
import logging
from datetime import datetime
from typing import Dict, Any
logger = logging.getLogger(__name__)
HISTORY_FILE = "rider_history.pkl"
class RiderHistoryService:
def __init__(self, history_file: str = HISTORY_FILE):
self.history_file = history_file
self.history = self._load_history()
def _load_history(self) -> Dict[int, Dict[str, float]]:
"""Load history from pickle file."""
if not os.path.exists(self.history_file):
return {}
try:
with open(self.history_file, 'rb') as f:
return pickle.load(f)
except Exception as e:
logger.error(f"Failed to load rider history: {e}")
return {}
def _save_history(self):
"""Save history to pickle file."""
try:
with open(self.history_file, 'wb') as f:
pickle.dump(self.history, f)
except Exception as e:
logger.error(f"Failed to save rider history: {e}")
def update_rider_stats(self, rider_id: int, distance_km: float, order_count: int):
"""Update cumulative stats for a rider."""
rider_id = int(rider_id)
if rider_id not in self.history:
self.history[rider_id] = {
"total_km": 0.0,
"total_orders": 0,
"last_updated": datetime.now().isoformat()
}
self.history[rider_id]["total_km"] += distance_km
self.history[rider_id]["total_orders"] += order_count
self.history[rider_id]["last_updated"] = datetime.now().isoformat()
# Auto-save on update
self._save_history()
def get_rider_score(self, rider_id: int) -> float:
"""
Get a score representing the rider's historical 'load' (KMs).
Higher Score = More KMs driven recently.
"""
rider_id = int(rider_id)
stats = self.history.get(rider_id, {})
return stats.get("total_km", 0.0)
def get_preferred_assignment_type(self, rider_id: int, all_rider_scores: Dict[int, float]) -> str:
"""
Determine if rider should get 'Long' or 'Short' routes based on population average.
"""
score = self.get_rider_score(rider_id)
if not all_rider_scores:
return "ANY"
avg_score = sum(all_rider_scores.values()) / len(all_rider_scores)
# If rider has driven LESS than average, prefer LONG routes (Risky)
if score < avg_score:
return "LONG"
# If rider has driven MORE than average, prefer SHORT routes (Economy)
else:
return "SHORT"

View File

@@ -0,0 +1,108 @@
import os
import pickle
import logging
import time
from datetime import datetime
from typing import Dict, Any, List, Set
logger = logging.getLogger(__name__)
STATE_FILE = "rider_active_state.pkl"
class RiderStateManager:
"""
Manages the 'Short-Term' Active State of Riders for session persistence.
Tracks:
- Minutes Committed (Remaining Workload)
- Active Kitchens (Unique Pickups in current queue)
- Last Planned Drop Location (for Daisy Chaining)
- Timestamp of last update (for Time Decay)
"""
def __init__(self, state_file: str = STATE_FILE):
self.state_file = state_file
self.states = self._load_states()
def _load_states(self) -> Dict[str, Any]:
"""Load states from pickle."""
if not os.path.exists(self.state_file):
return {}
try:
with open(self.state_file, 'rb') as f:
return pickle.load(f)
except Exception as e:
logger.error(f"Failed to load rider active states: {e}")
return {}
def _save_states(self):
"""Save states to pickle."""
try:
with open(self.state_file, 'wb') as f:
pickle.dump(self.states, f)
except Exception as e:
logger.error(f"Failed to save rider active states: {e}")
def get_rider_state(self, rider_id: int) -> Dict[str, Any]:
"""
Get the current active state of a rider with TIME DECAY applied.
If the server restarts after 30 mins, the 'minutes_committed' should reduce by 30.
"""
rider_id = int(rider_id)
raw_state = self.states.get(rider_id)
if not raw_state:
return {
'minutes_remaining': 0.0,
'last_drop_lat': None,
'last_drop_lon': None,
'active_kitchens': set(),
'last_updated_ts': time.time()
}
# Apply Time Decay
last_ts = raw_state.get('last_updated_ts', time.time())
current_ts = time.time()
elapsed_mins = (current_ts - last_ts) / 60.0
remaining = max(0.0, raw_state.get('minutes_remaining', 0.0) - elapsed_mins)
# If queue is empty, kitchens are cleared
kitchens = raw_state.get('active_kitchens', set())
if remaining <= 5.0: # Buffer: if almost done, free up kitchens
kitchens = set()
return {
'minutes_remaining': remaining,
'last_drop_lat': raw_state.get('last_drop_lat'),
'last_drop_lon': raw_state.get('last_drop_lon'),
'active_kitchens': kitchens,
'last_updated_ts': current_ts
}
def update_rider_state(self, rider_id: int, added_minutes: float, new_kitchens: Set[str], last_lat: float, last_lon: float):
"""
Update the state after a new assignment.
"""
rider_id = int(rider_id)
# Get current state (decayed)
current = self.get_rider_state(rider_id)
# Accumulate
updated_minutes = current['minutes_remaining'] + added_minutes
updated_kitchens = current['active_kitchens'].union(new_kitchens)
self.states[rider_id] = {
'minutes_remaining': updated_minutes,
'last_drop_lat': last_lat,
'last_drop_lon': last_lon,
'active_kitchens': updated_kitchens,
'last_updated_ts': time.time()
}
self._save_states()
def clear_state(self, rider_id: int):
rider_id = int(rider_id)
if rider_id in self.states:
del self.states[rider_id]
self._save_states()