Sherry245 commited on
Commit
b2f8c01
·
1 Parent(s): 7a9ec31

Initial Deploy

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +17 -0
  2. README.md +28 -5
  3. app/__init__.py +0 -0
  4. app/__pycache__/__init__.cpython-314.pyc +0 -0
  5. app/__pycache__/deps.cpython-314.pyc +0 -0
  6. app/__pycache__/main.cpython-314.pyc +0 -0
  7. app/api/__init__.py +0 -0
  8. app/api/__pycache__/__init__.cpython-314.pyc +0 -0
  9. app/api/v1/__init__.py +0 -0
  10. app/api/v1/__pycache__/__init__.cpython-314.pyc +0 -0
  11. app/api/v1/__pycache__/auth.cpython-314.pyc +0 -0
  12. app/api/v1/__pycache__/drivers.cpython-314.pyc +0 -0
  13. app/api/v1/__pycache__/payments.cpython-314.pyc +0 -0
  14. app/api/v1/__pycache__/rides.cpython-314.pyc +0 -0
  15. app/api/v1/__pycache__/users.cpython-314.pyc +0 -0
  16. app/api/v1/drivers.py +41 -0
  17. app/api/v1/payments.py +118 -0
  18. app/api/v1/rides.py +79 -0
  19. app/api/v1/users.py +20 -0
  20. app/core/__init__.py +0 -0
  21. app/core/__pycache__/__init__.cpython-314.pyc +0 -0
  22. app/core/__pycache__/config.cpython-314.pyc +0 -0
  23. app/core/__pycache__/database.cpython-314.pyc +0 -0
  24. app/core/__pycache__/security.cpython-314.pyc +0 -0
  25. app/core/config.py +20 -0
  26. app/core/database.py +11 -0
  27. app/deps.py +77 -0
  28. app/main.py +24 -0
  29. app/models/__init__.py +0 -0
  30. app/models/__pycache__/__init__.cpython-314.pyc +0 -0
  31. app/models/__pycache__/profile.cpython-314.pyc +0 -0
  32. app/models/__pycache__/ride.cpython-314.pyc +0 -0
  33. app/models/__pycache__/user.cpython-314.pyc +0 -0
  34. app/models/driver.py +16 -0
  35. app/models/payment.py +0 -0
  36. app/models/profile.py +19 -0
  37. app/models/ride.py +35 -0
  38. app/schemas/__init__.py +0 -0
  39. app/schemas/__pycache__/__init__.cpython-314.pyc +0 -0
  40. app/schemas/__pycache__/auth.cpython-314.pyc +0 -0
  41. app/schemas/__pycache__/drivers.cpython-314.pyc +0 -0
  42. app/schemas/__pycache__/payments.cpython-314.pyc +0 -0
  43. app/schemas/__pycache__/rides.cpython-314.pyc +0 -0
  44. app/schemas/__pycache__/users.cpython-314.pyc +0 -0
  45. app/schemas/drivers.py +8 -0
  46. app/schemas/payments.py +28 -0
  47. app/schemas/rides.py +49 -0
  48. app/schemas/users.py +5 -0
  49. app/services/__init__.py +0 -0
  50. app/services/__pycache__/__init__.cpython-314.pyc +0 -0
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ build-essential \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ COPY requirements.txt .
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
+
12
+ COPY app ./app
13
+ COPY alembic ./alembic
14
+
15
+ EXPOSE 8000
16
+
17
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
README.md CHANGED
@@ -1,11 +1,34 @@
1
  ---
2
- title: Rideshare Api
3
- emoji: 🌖
4
- colorFrom: yellow
5
- colorTo: red
6
  sdk: docker
 
7
  pinned: false
8
  license: apache-2.0
 
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: RideShare API
3
+ emoji: 🚕
4
+ colorFrom: indigo
5
+ colorTo: emerald
6
  sdk: docker
7
+ app_port: 8000
8
  pinned: false
9
  license: apache-2.0
10
+ short_description: FastAPI backend for the RideShare demo (rider, driver, payments).
11
  ---
12
 
13
+ # RideShare API
14
+
15
+ FastAPI backend powering the RideShare Android demo. PostgreSQL/PostGIS data lives in Supabase; this service exposes the REST + WebSocket surface the mobile app consumes.
16
+
17
+ ## Health check
18
+
19
+ ```
20
+ GET /health → {"status":"ok"}
21
+ ```
22
+
23
+ ## Required secrets (set in HF Space → Settings → Secrets)
24
+
25
+ | Key | Source |
26
+ |---|---|
27
+ | `DATABASE_URL` | Supabase Transaction Pooler URI with `postgresql+asyncpg://` prefix |
28
+ | `SUPABASE_URL` | Supabase project URL |
29
+ | `SUPABASE_JWT_SECRET` | Supabase → Settings → API → JWT Settings → JWT Secret |
30
+ | `GOOGLE_MAPS_KEY` | Google Cloud project key with Directions API enabled |
31
+ | `STRIPE_SECRET_KEY` | Stripe dashboard (test mode) |
32
+ | `STRIPE_PUBLISHABLE_KEY` | Stripe dashboard (test mode) |
33
+
34
+ App reads them at runtime via `pydantic-settings`. Missing required vars will crash on boot — check the build logs.
app/__init__.py ADDED
File without changes
app/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (179 Bytes). View file
 
app/__pycache__/deps.cpython-314.pyc ADDED
Binary file (4.91 kB). View file
 
app/__pycache__/main.cpython-314.pyc ADDED
Binary file (1.44 kB). View file
 
app/api/__init__.py ADDED
File without changes
app/api/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (183 Bytes). View file
 
app/api/v1/__init__.py ADDED
File without changes
app/api/v1/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (186 Bytes). View file
 
app/api/v1/__pycache__/auth.cpython-314.pyc ADDED
Binary file (3.5 kB). View file
 
app/api/v1/__pycache__/drivers.cpython-314.pyc ADDED
Binary file (3.79 kB). View file
 
app/api/v1/__pycache__/payments.cpython-314.pyc ADDED
Binary file (6.69 kB). View file
 
app/api/v1/__pycache__/rides.cpython-314.pyc ADDED
Binary file (4.58 kB). View file
 
app/api/v1/__pycache__/users.cpython-314.pyc ADDED
Binary file (1.2 kB). View file
 
app/api/v1/drivers.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from fastapi import APIRouter, Depends, HTTPException
3
+ from app.deps import get_current_user
4
+ from app.models.profile import Profile, UserRole
5
+ from app.schemas.drivers import LocationIn
6
+ from app.services.redis_client import r
7
+
8
+ router = APIRouter()
9
+
10
+
11
+ def _require_driver(user: Profile) -> None:
12
+ if user.role != UserRole.driver:
13
+ raise HTTPException(403, 'Driver role required')
14
+
15
+
16
+ @router.post('/online')
17
+ async def go_online(loc: LocationIn, user: Profile = Depends(get_current_user)):
18
+ _require_driver(user)
19
+ await r.geoadd('drivers:online', (loc.lng, loc.lat, str(user.id)))
20
+ await r.hset(f'driver:{user.id}', mapping={'lat': loc.lat, 'lng': loc.lng})
21
+ return {'ok': True}
22
+
23
+
24
+ @router.post('/offline')
25
+ async def go_offline(user: Profile = Depends(get_current_user)):
26
+ _require_driver(user)
27
+ await r.zrem('drivers:online', str(user.id))
28
+ await r.delete(f'driver:{user.id}')
29
+ return {'ok': True}
30
+
31
+
32
+ @router.post('/location')
33
+ async def update_location(loc: LocationIn, user: Profile = Depends(get_current_user)):
34
+ _require_driver(user)
35
+ await r.geoadd('drivers:online', (loc.lng, loc.lat, str(user.id)))
36
+ if loc.ride_id is not None:
37
+ await r.publish(
38
+ f'ride:{loc.ride_id}:driver_location',
39
+ json.dumps({'lat': loc.lat, 'lng': loc.lng}),
40
+ )
41
+ return {'ok': True}
app/api/v1/payments.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from datetime import UTC, datetime
3
+
4
+ import stripe
5
+ from fastapi import APIRouter, Depends, HTTPException
6
+ from sqlalchemy.ext.asyncio import AsyncSession
7
+
8
+ from app.core.config import settings
9
+ from app.core.database import get_db
10
+ from app.deps import get_current_user
11
+ from app.models.profile import Profile
12
+ from app.models.ride import Ride, RideStatus
13
+ from app.schemas.payments import (
14
+ ConfirmRidePaymentIn,
15
+ InitRidePaymentIn,
16
+ InitRidePaymentOut,
17
+ TestChargeIn,
18
+ TestChargeOut,
19
+ )
20
+ from app.services.stripe_service import (
21
+ create_ephemeral_key,
22
+ create_payment_intent,
23
+ ensure_customer,
24
+ )
25
+ from app.websockets.manager import manager
26
+
27
+ logger = logging.getLogger(__name__)
28
+ router = APIRouter()
29
+
30
+
31
+ @router.post('/init-ride-payment', response_model=InitRidePaymentOut)
32
+ async def init_ride_payment(
33
+ data: InitRidePaymentIn,
34
+ user: Profile = Depends(get_current_user),
35
+ db: AsyncSession = Depends(get_db),
36
+ ):
37
+ ride = await db.get(Ride, data.ride_id)
38
+ if ride is None:
39
+ raise HTTPException(404, 'Ride not found')
40
+ if ride.rider_id != user.id:
41
+ raise HTTPException(403, 'Only the rider can pay for this ride')
42
+ if ride.fare is None or ride.fare <= 0:
43
+ raise HTTPException(400, 'Ride has no fare')
44
+
45
+ try:
46
+ customer_id = ensure_customer(user)
47
+ intent = create_payment_intent(
48
+ amount=ride.fare, customer_id=customer_id, ride_id=str(ride.id)
49
+ )
50
+ ephemeral = create_ephemeral_key(customer_id)
51
+ except stripe.StripeError as exc:
52
+ logger.exception('Stripe error initialising ride payment')
53
+ raise HTTPException(502, f'Stripe error: {exc.user_message or str(exc)}') from exc
54
+
55
+ ride.payment_intent_id = intent['id']
56
+ await db.commit()
57
+
58
+ return InitRidePaymentOut(
59
+ payment_intent_id=intent['id'],
60
+ client_secret=intent['client_secret'],
61
+ ephemeral_key=ephemeral,
62
+ customer_id=customer_id,
63
+ publishable_key=settings.STRIPE_PUBLISHABLE_KEY,
64
+ )
65
+
66
+
67
+ @router.post('/confirm-ride-payment')
68
+ async def confirm_ride_payment(
69
+ data: ConfirmRidePaymentIn,
70
+ user: Profile = Depends(get_current_user),
71
+ db: AsyncSession = Depends(get_db),
72
+ ):
73
+ ride = await db.get(Ride, data.ride_id)
74
+ if ride is None:
75
+ raise HTTPException(404, 'Ride not found')
76
+ if ride.rider_id != user.id:
77
+ raise HTTPException(403, 'Only the rider can confirm this payment')
78
+
79
+ ride.status = RideStatus.completed
80
+ ride.ended_at = datetime.now(UTC)
81
+ await db.commit()
82
+
83
+ receipt = {
84
+ 'type': 'ride_paid',
85
+ 'ride_id': str(ride.id),
86
+ 'fare': ride.fare,
87
+ }
88
+ await manager.send(str(ride.rider_id), receipt)
89
+ if ride.driver_id is not None:
90
+ await manager.send(str(ride.driver_id), receipt)
91
+
92
+ return {'ok': True}
93
+
94
+
95
+ @router.post('/test-charge', response_model=TestChargeOut)
96
+ async def test_charge(
97
+ data: TestChargeIn,
98
+ user: Profile = Depends(get_current_user),
99
+ db: AsyncSession = Depends(get_db),
100
+ ):
101
+ if data.amount <= 0:
102
+ raise HTTPException(400, 'Amount must be greater than zero')
103
+
104
+ try:
105
+ customer_id = ensure_customer(user)
106
+ intent = stripe.PaymentIntent.create(
107
+ amount=int(data.amount * 100),
108
+ currency=data.currency,
109
+ customer=customer_id,
110
+ automatic_payment_methods={'enabled': True, 'allow_redirects': 'never'},
111
+ metadata={'profile_id': str(user.id), 'kind': 'test_charge'},
112
+ )
113
+ except stripe.StripeError as exc:
114
+ logger.exception('Stripe error creating test charge')
115
+ raise HTTPException(502, f'Stripe error: {exc.user_message or str(exc)}') from exc
116
+
117
+ await db.commit()
118
+ return TestChargeOut(payment_intent_id=intent['id'], client_secret=intent['client_secret'])
app/api/v1/rides.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import UTC, datetime
2
+ from fastapi import APIRouter, Depends, HTTPException
3
+ from geoalchemy2 import WKTElement
4
+ from sqlalchemy.exc import SQLAlchemyError
5
+ from sqlalchemy.ext.asyncio import AsyncSession
6
+
7
+ from app.core.database import get_db
8
+ from app.deps import get_current_user
9
+ from app.models.ride import Ride
10
+ from app.services.maps import google_directions
11
+ from app.services.matching import find_nearest_driver
12
+ from app.services.pricing import estimate_fare
13
+ from app.schemas.rides import EstimateIn, EstimateOut, RideRequestIn, RideOut
14
+
15
+ router = APIRouter()
16
+
17
+
18
+ @router.post('/estimate', response_model=EstimateOut)
19
+ async def estimate(data: EstimateIn):
20
+ try:
21
+ distance_km, duration_min = await google_directions(data.pickup, data.dropoff)
22
+ except Exception as exc:
23
+ raise HTTPException(400, 'Unable to estimate this route') from exc
24
+
25
+ return EstimateOut(
26
+ distance_km=distance_km,
27
+ duration_min=duration_min,
28
+ fare=estimate_fare(distance_km, duration_min, data.vehicle_type),
29
+ vehicle_type=data.vehicle_type,
30
+ )
31
+
32
+
33
+ @router.post('/request', response_model=RideOut)
34
+ async def request_ride(
35
+ data: RideRequestIn,
36
+ user=Depends(get_current_user),
37
+ db: AsyncSession = Depends(get_db),
38
+ ):
39
+ ride = Ride(
40
+ rider_id=user.id,
41
+ pickup=WKTElement(f'POINT({data.pickup.lng} {data.pickup.lat})', srid=4326),
42
+ dropoff=WKTElement(f'POINT({data.dropoff.lng} {data.dropoff.lat})', srid=4326),
43
+ pickup_address=data.pickup_address,
44
+ dropoff_address=data.dropoff_address,
45
+ fare=data.fare,
46
+ distance_km=data.distance_km,
47
+ duration_min=data.duration_min,
48
+ vehicle_type=data.vehicle_type,
49
+ )
50
+ db.add(ride)
51
+ try:
52
+ await db.commit()
53
+ await db.refresh(ride)
54
+ except SQLAlchemyError as exc:
55
+ await db.rollback()
56
+ raise HTTPException(400, 'Unable to save ride request') from exc
57
+
58
+ try:
59
+ await find_nearest_driver(ride.id, data.pickup)
60
+ except HTTPException as exc:
61
+ if exc.status_code != 404:
62
+ raise
63
+ except Exception:
64
+ # Ride creation should still succeed if live driver matching is temporarily unavailable.
65
+ pass
66
+
67
+ return RideOut(
68
+ id=ride.id,
69
+ rider_id=ride.rider_id,
70
+ driver_id=ride.driver_id,
71
+ status=ride.status,
72
+ fare=ride.fare,
73
+ distance_km=ride.distance_km,
74
+ duration_min=ride.duration_min,
75
+ pickup_address=ride.pickup_address,
76
+ dropoff_address=ride.dropoff_address,
77
+ vehicle_type=ride.vehicle_type,
78
+ requested_at=ride.requested_at or datetime.now(UTC),
79
+ )
app/api/v1/users.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+
4
+ from app.core.database import get_db
5
+ from app.deps import get_current_user
6
+ from app.models.profile import Profile
7
+ from app.schemas.users import PushTokenIn
8
+
9
+ router = APIRouter()
10
+
11
+
12
+ @router.post('/push-token')
13
+ async def save_push_token(
14
+ data: PushTokenIn,
15
+ user: Profile = Depends(get_current_user),
16
+ db: AsyncSession = Depends(get_db),
17
+ ):
18
+ user.push_token = data.token
19
+ await db.commit()
20
+ return {'ok': True}
app/core/__init__.py ADDED
File without changes
app/core/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (184 Bytes). View file
 
app/core/__pycache__/config.cpython-314.pyc ADDED
Binary file (1.35 kB). View file
 
app/core/__pycache__/database.cpython-314.pyc ADDED
Binary file (991 Bytes). View file
 
app/core/__pycache__/security.cpython-314.pyc ADDED
Binary file (2.62 kB). View file
 
app/core/config.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings
2
+
3
+
4
+ class Settings(BaseSettings):
5
+ DATABASE_URL: str
6
+ REDIS_URL: str = 'redis://localhost:6379/0'
7
+
8
+ SUPABASE_URL: str
9
+ SUPABASE_JWT_SECRET: str
10
+ SUPABASE_JWT_ALGORITHM: str = 'HS256'
11
+
12
+ GOOGLE_MAPS_KEY: str
13
+ STRIPE_SECRET_KEY: str
14
+ STRIPE_PUBLISHABLE_KEY: str
15
+
16
+ class Config:
17
+ env_file = '.env'
18
+
19
+
20
+ settings = Settings()
app/core/database.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
2
+ from sqlalchemy.orm import sessionmaker, declarative_base
3
+ from app.core.config import settings
4
+
5
+ engine = create_async_engine(settings.DATABASE_URL, echo=False)
6
+ AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
7
+ Base = declarative_base()
8
+
9
+ async def get_db():
10
+ async with AsyncSessionLocal() as session:
11
+ yield session
app/deps.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ from fastapi import Depends, HTTPException
3
+ from fastapi.security import OAuth2PasswordBearer
4
+ from jose import jwt, JWTError
5
+ import requests
6
+ from sqlalchemy.exc import SQLAlchemyError
7
+ from app.core.config import settings
8
+ from app.core.database import get_db
9
+ from app.models.profile import Profile, UserRole
10
+
11
+ oauth2 = OAuth2PasswordBearer(tokenUrl='supabase')
12
+
13
+
14
+ def verify_supabase_token(token: str = Depends(oauth2)) -> dict:
15
+ try:
16
+ header = jwt.get_unverified_header(token)
17
+ algorithm = header.get('alg')
18
+ if not algorithm:
19
+ raise JWTError('Missing token algorithm')
20
+
21
+ if algorithm == settings.SUPABASE_JWT_ALGORITHM:
22
+ payload = jwt.decode(
23
+ token,
24
+ settings.SUPABASE_JWT_SECRET,
25
+ algorithms=[settings.SUPABASE_JWT_ALGORITHM],
26
+ audience='authenticated',
27
+ )
28
+ else:
29
+ jwks_url = f'{settings.SUPABASE_URL.rstrip("/")}/auth/v1/.well-known/jwks.json'
30
+ response = requests.get(jwks_url, timeout=5)
31
+ response.raise_for_status()
32
+ keys = response.json().get('keys', [])
33
+ key = next((item for item in keys if item.get('kid') == header.get('kid')), None)
34
+ if not key:
35
+ raise JWTError('No matching JWKS key')
36
+ payload = jwt.decode(
37
+ token,
38
+ key,
39
+ algorithms=[algorithm],
40
+ audience='authenticated',
41
+ issuer=f'{settings.SUPABASE_URL.rstrip("/")}/auth/v1',
42
+ )
43
+ uuid.UUID(payload['sub'])
44
+ return payload
45
+ except (JWTError, KeyError, ValueError, requests.RequestException):
46
+ raise HTTPException(401, 'Invalid token')
47
+
48
+
49
+ async def get_current_user(payload: dict = Depends(verify_supabase_token), db=Depends(get_db)) -> Profile:
50
+ user_id = uuid.UUID(payload['sub'])
51
+
52
+ try:
53
+ profile = await db.get(Profile, user_id)
54
+ except SQLAlchemyError as exc:
55
+ raise HTTPException(503, 'Unable to load user profile') from exc
56
+
57
+ if not profile:
58
+ metadata = payload.get('user_metadata') or {}
59
+ try:
60
+ role = UserRole(metadata.get('role'))
61
+ except ValueError:
62
+ role = UserRole.rider
63
+
64
+ profile = Profile(
65
+ id=user_id,
66
+ full_name=metadata.get('full_name') or payload.get('email') or 'Rider',
67
+ role=role,
68
+ )
69
+ db.add(profile)
70
+ try:
71
+ await db.commit()
72
+ await db.refresh(profile)
73
+ except SQLAlchemyError as exc:
74
+ await db.rollback()
75
+ raise HTTPException(503, 'Unable to create user profile') from exc
76
+
77
+ return profile
app/main.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from app.api.v1 import rides, drivers, payments, users
4
+ from app.websockets import routes as ws_routes
5
+
6
+ app = FastAPI(title='RideShare API', version='1.0.0')
7
+
8
+ app.add_middleware(
9
+ CORSMiddleware,
10
+ allow_origins=['*'],
11
+ allow_methods=['*'],
12
+ allow_headers=['*'],
13
+ )
14
+
15
+ app.include_router(rides.router, prefix='/api/v1/rides', tags=['rides'])
16
+ app.include_router(drivers.router, prefix='/api/v1/drivers', tags=['drivers'])
17
+ app.include_router(payments.router, prefix='/api/v1/payments', tags=['payments'])
18
+ app.include_router(users.router, prefix='/api/v1/users', tags=['users'])
19
+ app.include_router(ws_routes.router)
20
+
21
+
22
+ @app.get('/health')
23
+ async def health():
24
+ return {'status': 'ok'}
app/models/__init__.py ADDED
File without changes
app/models/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (186 Bytes). View file
 
app/models/__pycache__/profile.cpython-314.pyc ADDED
Binary file (1.42 kB). View file
 
app/models/__pycache__/ride.cpython-314.pyc ADDED
Binary file (2.39 kB). View file
 
app/models/__pycache__/user.cpython-314.pyc ADDED
Binary file (1.49 kB). View file
 
app/models/driver.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, ForeignKey, Boolean, Float, String
2
+ from sqlalchemy.dialects.postgresql import UUID
3
+ from geoalchemy2 import Geography
4
+ from app.core.database import Base
5
+
6
+
7
+ class Driver(Base):
8
+ __tablename__ = 'drivers'
9
+ user_id = Column(UUID(as_uuid=True), ForeignKey('profiles.id', ondelete='CASCADE'), primary_key=True)
10
+ license_no = Column(String, nullable=False)
11
+ vehicle_make = Column(String)
12
+ vehicle_model = Column(String)
13
+ vehicle_plate = Column(String)
14
+ is_online = Column(Boolean, default=False)
15
+ rating = Column(Float, default=5.0)
16
+ current_location = Column(Geography('POINT', srid=4326))
app/models/payment.py ADDED
File without changes
app/models/profile.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import enum
2
+ from sqlalchemy import Column, String, DateTime, Enum, func
3
+ from sqlalchemy.dialects.postgresql import UUID
4
+ from app.core.database import Base
5
+
6
+
7
+ class UserRole(str, enum.Enum):
8
+ rider = 'rider'
9
+ driver = 'driver'
10
+
11
+
12
+ class Profile(Base):
13
+ __tablename__ = 'profiles'
14
+ id = Column(UUID(as_uuid=True), primary_key=True)
15
+ full_name = Column(String, nullable=False)
16
+ role = Column(Enum(UserRole, name='user_role'), nullable=False)
17
+ stripe_customer_id = Column(String, nullable=True)
18
+ push_token = Column(String, nullable=True)
19
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
app/models/ride.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ import enum
3
+ from sqlalchemy import Column, ForeignKey, Float, String, DateTime, Enum, func
4
+ from sqlalchemy.dialects.postgresql import UUID
5
+ from geoalchemy2 import Geography
6
+ from app.core.database import Base
7
+
8
+
9
+ class RideStatus(str, enum.Enum):
10
+ searching = 'searching'
11
+ accepted = 'accepted'
12
+ arriving = 'arriving'
13
+ in_progress = 'in_progress'
14
+ completed = 'completed'
15
+ cancelled = 'cancelled'
16
+
17
+
18
+ class Ride(Base):
19
+ __tablename__ = 'rides'
20
+ id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
21
+ rider_id = Column(UUID(as_uuid=True), ForeignKey('profiles.id', ondelete='CASCADE'), nullable=False)
22
+ driver_id = Column(UUID(as_uuid=True), ForeignKey('profiles.id', ondelete='SET NULL'), nullable=True)
23
+ pickup = Column(Geography('POINT', srid=4326))
24
+ dropoff = Column(Geography('POINT', srid=4326))
25
+ pickup_address = Column(String)
26
+ dropoff_address = Column(String)
27
+ status = Column(Enum(RideStatus, name='ride_status'), default=RideStatus.searching, nullable=False)
28
+ fare = Column(Float)
29
+ distance_km = Column(Float)
30
+ duration_min = Column(Float)
31
+ payment_intent_id = Column(String, nullable=True)
32
+ vehicle_type = Column(String, default='car')
33
+ requested_at = Column(DateTime(timezone=True), server_default=func.now())
34
+ started_at = Column(DateTime(timezone=True), nullable=True)
35
+ ended_at = Column(DateTime(timezone=True), nullable=True)
app/schemas/__init__.py ADDED
File without changes
app/schemas/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (187 Bytes). View file
 
app/schemas/__pycache__/auth.cpython-314.pyc ADDED
Binary file (1.86 kB). View file
 
app/schemas/__pycache__/drivers.cpython-314.pyc ADDED
Binary file (874 Bytes). View file
 
app/schemas/__pycache__/payments.cpython-314.pyc ADDED
Binary file (2.51 kB). View file
 
app/schemas/__pycache__/rides.cpython-314.pyc ADDED
Binary file (3.56 kB). View file
 
app/schemas/__pycache__/users.cpython-314.pyc ADDED
Binary file (720 Bytes). View file
 
app/schemas/drivers.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ from pydantic import BaseModel
3
+
4
+
5
+ class LocationIn(BaseModel):
6
+ lat: float
7
+ lng: float
8
+ ride_id: uuid.UUID | None = None
app/schemas/payments.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ from pydantic import BaseModel
3
+
4
+
5
+ class InitRidePaymentIn(BaseModel):
6
+ ride_id: uuid.UUID
7
+
8
+
9
+ class InitRidePaymentOut(BaseModel):
10
+ payment_intent_id: str
11
+ client_secret: str
12
+ ephemeral_key: str
13
+ customer_id: str
14
+ publishable_key: str
15
+
16
+
17
+ class ConfirmRidePaymentIn(BaseModel):
18
+ ride_id: uuid.UUID
19
+
20
+
21
+ class TestChargeIn(BaseModel):
22
+ amount: float
23
+ currency: str = 'pkr'
24
+
25
+
26
+ class TestChargeOut(BaseModel):
27
+ payment_intent_id: str
28
+ client_secret: str
app/schemas/rides.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ from datetime import datetime
3
+ from pydantic import BaseModel, ConfigDict
4
+ from app.models.ride import RideStatus
5
+
6
+
7
+ class Coordinate(BaseModel):
8
+ lat: float
9
+ lng: float
10
+
11
+
12
+ class EstimateIn(BaseModel):
13
+ pickup: Coordinate
14
+ dropoff: Coordinate
15
+ vehicle_type: str = 'car'
16
+
17
+
18
+ class EstimateOut(BaseModel):
19
+ distance_km: float
20
+ duration_min: float
21
+ fare: float
22
+ vehicle_type: str = 'car'
23
+
24
+
25
+ class RideRequestIn(BaseModel):
26
+ pickup: Coordinate
27
+ dropoff: Coordinate
28
+ pickup_address: str
29
+ dropoff_address: str
30
+ fare: float
31
+ distance_km: float
32
+ duration_min: float
33
+ vehicle_type: str = 'car'
34
+
35
+
36
+ class RideOut(BaseModel):
37
+ model_config = ConfigDict(from_attributes=True)
38
+
39
+ id: uuid.UUID
40
+ rider_id: uuid.UUID
41
+ driver_id: uuid.UUID | None = None
42
+ status: RideStatus
43
+ fare: float | None = None
44
+ distance_km: float | None = None
45
+ duration_min: float | None = None
46
+ pickup_address: str | None = None
47
+ dropoff_address: str | None = None
48
+ vehicle_type: str | None = None
49
+ requested_at: datetime
app/schemas/users.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class PushTokenIn(BaseModel):
5
+ token: str
app/services/__init__.py ADDED
File without changes
app/services/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (188 Bytes). View file