Skip to content

Commit 0d808c7

Browse files
authored
feat: Add create_gtfs_rt_feed endpoint to operations API (#1435)
1 parent a1e3b60 commit 0d808c7

File tree

9 files changed

+410
-79
lines changed

9 files changed

+410
-79
lines changed

api/src/shared/db_models/gtfs_rt_feed_impl.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import logging
2+
3+
from sqlalchemy.orm import Session
4+
5+
from shared.database.database import with_db_session
16
from shared.db_models.feed_impl import FeedImpl
2-
from shared.database_gen.sqlacodegen_models import Gtfsrealtimefeed as GtfsRTFeedOrm
7+
from shared.database_gen.sqlacodegen_models import Gtfsrealtimefeed, Feed as FeedOrm, Entitytype
38
from shared.db_models.location_impl import LocationImpl
49
from feeds_gen.models.gtfs_rt_feed import GtfsRTFeed
510

@@ -14,11 +19,42 @@ class Config:
1419
from_attributes = True
1520

1621
@classmethod
17-
def from_orm(cls, feed: GtfsRTFeedOrm | None) -> GtfsRTFeed | None:
22+
def from_orm(cls, feed: Gtfsrealtimefeed | None) -> GtfsRTFeed | None:
1823
gtfs_rt_feed: GtfsRTFeed = super().from_orm(feed)
1924
if not gtfs_rt_feed:
2025
return None
2126
gtfs_rt_feed.locations = [LocationImpl.from_orm(item) for item in feed.locations] if feed.locations else []
2227
gtfs_rt_feed.entity_types = [item.name for item in feed.entitytypes] if feed.entitytypes else []
2328
gtfs_rt_feed.feed_references = [item.stable_id for item in feed.gtfs_feeds] if feed.gtfs_feeds else []
2429
return gtfs_rt_feed
30+
31+
@classmethod
32+
@with_db_session
33+
def to_orm_entity_types(cls, entity_types: list, db_session: Session) -> list[Entitytype]:
34+
"""Convert the entity_types list to a list of Entitytype ORM objects."""
35+
if not entity_types:
36+
return []
37+
orm_entity_types = []
38+
for entity_type_name in entity_types:
39+
entity_type_orm = db_session.query(Entitytype).filter(Entitytype.name == entity_type_name).one_or_none()
40+
if entity_type_orm:
41+
orm_entity_types.append(entity_type_orm)
42+
else:
43+
logging.warning("Entity Type not found: %s.", entity_type_name)
44+
return orm_entity_types
45+
46+
@classmethod
47+
def to_orm_from_dict(cls, feed_dict: dict) -> Gtfsrealtimefeed | None:
48+
"""Convert a dictionary representation of a GTFS RT feed to a SQLAlchemy GtfsRTFeed ORM object."""
49+
if not feed_dict:
50+
return None
51+
feed: FeedOrm = super().to_orm_from_dict(feed_dict)
52+
if not feed:
53+
return None
54+
allowed = {col.name for col in Gtfsrealtimefeed.__mapper__.columns} | {
55+
rel.key for rel in Gtfsrealtimefeed.__mapper__.relationships
56+
}
57+
data = {k: v for k, v in feed.__dict__.items() if k in allowed}
58+
result = Gtfsrealtimefeed(**data)
59+
result.entitytypes = cls.to_orm_entity_types(feed_dict.get("entity_types"))
60+
return result

docs/OperationsAPI.yaml

Lines changed: 113 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ paths:
9595
201:
9696
description: >
9797
The feed was successfully created. No content is returned.
98+
9899
content:
99100
application/json:
100101
schema:
@@ -189,6 +190,43 @@ paths:
189190
schema:
190191
$ref: "#/components/schemas/OperationGtfsRtFeed"
191192
/v1/operations/feeds/gtfs_rt:
193+
post:
194+
description: Create a GTFS-RT feed in the Mobility Database.
195+
tags:
196+
- "operations"
197+
operationId: createGtfsRtFeed
198+
security:
199+
- ApiKeyAuth: []
200+
requestBody:
201+
description: Payload to create the specified GTF-RT feed.
202+
required: true
203+
content:
204+
application/json:
205+
schema:
206+
$ref: "#/components/schemas/OperationCreateRequestGtfsRtFeed"
207+
responses:
208+
201:
209+
description: >
210+
The feed was successfully created. No content is returned.
211+
212+
content:
213+
application/json:
214+
schema:
215+
$ref: "#/components/schemas/OperationGtfsRtFeed"
216+
400:
217+
description: >
218+
The request was invalid.
219+
220+
401:
221+
description: >
222+
The request was not authenticated or has invalid authentication credentials.
223+
224+
409:
225+
description: >
226+
A feed with the producer_url already exists.
227+
228+
500:
229+
description: "An internal server error occurred. \n"
192230
put:
193231
description: Update the specified GTFS-RT feed in the Mobility Database.
194232
tags:
@@ -1089,6 +1127,81 @@ components:
10891127
required:
10901128
- source_info
10911129
- operational_status
1130+
OperationCreateRequestGtfsRtFeed:
1131+
x-operation: true
1132+
type: object
1133+
properties:
1134+
status:
1135+
$ref: "#/components/schemas/FeedStatus"
1136+
external_ids:
1137+
$ref: "#/components/schemas/ExternalIds"
1138+
provider:
1139+
description: A commonly used name for the transit provider included in the feed.
1140+
type: string
1141+
example: Los Angeles Department of Transportation (LADOT, DASH, Commuter Express)
1142+
feed_name:
1143+
description: >
1144+
An optional description of the data feed, e.g to specify if the data feed is an aggregate of multiple providers, or which network is represented by the feed.
1145+
type: string
1146+
example: Bus
1147+
note:
1148+
description: A note to clarify complex use cases for consumers.
1149+
type: string
1150+
feed_contact_email:
1151+
description: Use to contact the feed producer.
1152+
type: string
1153+
1154+
source_info:
1155+
allOf:
1156+
- $ref: "#/components/schemas/SourceInfo"
1157+
- type: object
1158+
required:
1159+
- producer_url
1160+
redirects:
1161+
type: array
1162+
items:
1163+
$ref: "#/components/schemas/Redirect"
1164+
operational_status:
1165+
type: string
1166+
enum: [wip, published, unpublished]
1167+
default: wip
1168+
description: Current operational status of the feed.
1169+
official:
1170+
type: boolean
1171+
description: Whether this is an official feed.
1172+
entity_types:
1173+
type: array
1174+
minItems: 1
1175+
items:
1176+
type: string
1177+
enum:
1178+
- vp
1179+
- tu
1180+
- sa
1181+
example: vp
1182+
description: >
1183+
The type of realtime entry:
1184+
* vp - vehicle positions
1185+
* tu - trip updates
1186+
* sa - service alerts
1187+
feed_references:
1188+
description: A list of the GTFS feeds that the real time source is associated with, represented by their MDB source IDs.
1189+
type: array
1190+
items:
1191+
type: string
1192+
example: "mdb-20"
1193+
locations:
1194+
$ref: "#/components/schemas/Locations"
1195+
related_links:
1196+
description: >
1197+
A list of related links for the feed.
1198+
type: array
1199+
items:
1200+
$ref: '#/components/schemas/FeedRelatedLink'
1201+
required:
1202+
- source_info
1203+
- operational_status
1204+
- entity_types
10921205
OperationFeed:
10931206
x-operation: true
10941207
allOf:
@@ -1185,11 +1298,6 @@ components:
11851298
example: vp
11861299
description: >
11871300
The type of realtime entry:
1188-
1189-
1190-
1191-
1192-
11931301
* vp - vehicle positions
11941302
* tu - trip updates
11951303
* sa - service alerts
@@ -1265,12 +1373,6 @@ components:
12651373
x-operation: true
12661374
description: >
12671375
Describes status of the Feed. Should be one of
1268-
1269-
1270-
1271-
1272-
1273-
12741376
* `active` Feed should be used in public trip planners.
12751377
* `deprecated` Feed is explicitly deprecated and should not be used in public trip planners.
12761378
* `inactive` Feed hasn't been recently updated and should be used at risk of providing outdated information.
@@ -1288,12 +1390,6 @@ components:
12881390
x-operation: true
12891391
description: >
12901392
Describes data type of a feed. Should be one of
1291-
1292-
1293-
1294-
1295-
1296-
12971393
* `gtfs` GTFS feed.
12981394
* `gtfs_rt` GTFS-RT feed.
12991395
* `gbfs` GBFS feed.

functions-python/operations_api/.openapi-generator/FILES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ src/feeds_gen/models/location.py
2626
src/feeds_gen/models/metadata.py
2727
src/feeds_gen/models/operation_create_request_gtfs_feed.py
2828
src/feeds_gen/models/operation_create_request_gtfs_feed_source_info.py
29+
src/feeds_gen/models/operation_create_request_gtfs_rt_feed.py
2930
src/feeds_gen/models/operation_feed.py
3031
src/feeds_gen/models/operation_gtfs_feed.py
3132
src/feeds_gen/models/operation_gtfs_rt_feed.py

functions-python/operations_api/src/feeds_operations/impl/feeds_operations_impl.py

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
from feeds_gen.models.operation_create_request_gtfs_feed import (
3232
OperationCreateRequestGtfsFeed,
3333
)
34+
from feeds_gen.models.operation_create_request_gtfs_rt_feed import (
35+
OperationCreateRequestGtfsRtFeed,
36+
)
3437
from feeds_gen.models.operation_gtfs_feed import OperationGtfsFeed
3538
from feeds_gen.models.operation_gtfs_rt_feed import OperationGtfsRtFeed
3639
from feeds_operations.impl.models.update_request_gtfs_feed_impl import (
@@ -59,6 +62,9 @@
5962
from .models.operation_create_request_gtfs_feed import (
6063
OperationCreateRequestGtfsFeedImpl,
6164
)
65+
from .models.operation_create_request_gtfs_rt_feed import (
66+
OperationCreateRequestGtfsRtFeedImpl,
67+
)
6268
from .models.operation_feed_impl import OperationFeedImpl
6369
from .models.operation_gtfs_feed_impl import OperationGtfsFeedImpl
6470
from .models.operation_gtfs_rt_feed_impl import OperationGtfsRtFeedImpl
@@ -68,6 +74,37 @@
6874
class OperationsApiImpl(BaseOperationsApi):
6975
"""Implementation of the operations API."""
7076

77+
@staticmethod
78+
def assign_feed_id(new_feed: Gtfsfeed | Gtfsrealtimefeed):
79+
client_provided_id = bool(getattr(new_feed, "id", None))
80+
if not client_provided_id:
81+
new_feed.id = new_feed.stable_id
82+
83+
@staticmethod
84+
def assign_stable_id(new_feed: Gtfsfeed | Gtfsrealtimefeed, db_session: Session):
85+
client_provided_stable_id = bool(getattr(new_feed, "stable_id", None))
86+
if not client_provided_stable_id:
87+
next_val = db_session.execute(
88+
text("SELECT nextval('md_sequence')")
89+
).scalar_one()
90+
new_feed.stable_id = f"md-{next_val}"
91+
92+
@staticmethod
93+
def assert_no_existing_feed_url(producer_url: str, db_session: Session):
94+
existing_feed = get_feed_by_normalized_url(producer_url, db_session)
95+
if existing_feed:
96+
message = (
97+
f"A published feed with url "
98+
f"{producer_url} already exists."
99+
f"Existing feed ID: {existing_feed.stable_id}, "
100+
f"URL: {existing_feed.producer_url}"
101+
)
102+
logging.error(message)
103+
raise HTTPException(
104+
status_code=400,
105+
detail=message,
106+
)
107+
71108
@with_db_session
72109
async def get_feeds(
73110
self,
@@ -314,38 +351,48 @@ async def create_gtfs_feed(
314351
) -> OperationGtfsFeed:
315352
"""Create a GTFS feed in the Mobility Database."""
316353
# Check if the provider_url already exists in an active feed
317-
existing_feed = get_feed_by_normalized_url(
318-
operation_create_request_gtfs_feed.source_info.producer_url, db_session
354+
OperationsApiImpl.assert_no_existing_feed_url(
355+
operation_create_request_gtfs_feed.source_info.producer_url,
356+
db_session,
319357
)
320-
if existing_feed:
321-
message = (
322-
f"A published feed with url "
323-
f"{operation_create_request_gtfs_feed.source_info.producer_url} already exists."
324-
f"Existing feed ID: {existing_feed.stable_id}, "
325-
f"URL: {existing_feed.producer_url}"
326-
)
327-
logging.error(message)
328-
raise HTTPException(
329-
status_code=400,
330-
detail=message,
331-
)
332358
# Proceed with feed creation
333359
new_feed = OperationCreateRequestGtfsFeedImpl.to_orm(
334360
operation_create_request_gtfs_feed
335361
)
336362
new_feed.data_type = DataType.GTFS.value
337-
client_provided_stable_id = bool(getattr(new_feed, "stable_id", None))
338-
if not client_provided_stable_id:
339-
next_val = db_session.execute(
340-
text("SELECT nextval('md_sequence')")
341-
).scalar_one()
342-
new_feed.stable_id = f"md-{next_val}"
343-
client_provided_id = bool(getattr(new_feed, "id", None))
344-
if not client_provided_id:
345-
new_feed.id = new_feed.stable_id
363+
OperationsApiImpl.assign_stable_id(new_feed, db_session)
364+
OperationsApiImpl.assign_feed_id(new_feed)
346365
db_session.add(new_feed)
347366
db_session.commit()
348367
created_feed = db_session.get(Gtfsfeed, new_feed.id)
349368
logging.info("Created new GTFS feed with ID: %s", new_feed.stable_id)
350369
payload = OperationGtfsFeedImpl.from_orm(created_feed).model_dump()
351370
return JSONResponse(status_code=201, content=jsonable_encoder(payload))
371+
372+
@with_db_session
373+
async def create_gtfs_rt_feed(
374+
self,
375+
operation_create_request_gtfs_rt_feed: Annotated[
376+
OperationCreateRequestGtfsRtFeed,
377+
Field(description="Payload to create the specified GTF-RT feed."),
378+
],
379+
db_session: Session = None,
380+
) -> OperationGtfsRtFeed:
381+
"""Create a GTFS-RT feed in the Mobility Database."""
382+
OperationsApiImpl.assert_no_existing_feed_url(
383+
operation_create_request_gtfs_rt_feed.source_info.producer_url,
384+
db_session,
385+
)
386+
# Proceed with feed creation
387+
new_feed = OperationCreateRequestGtfsRtFeedImpl.to_orm(
388+
operation_create_request_gtfs_rt_feed
389+
)
390+
new_feed.data_type = DataType.GTFS_RT.value
391+
OperationsApiImpl.assign_stable_id(new_feed, db_session)
392+
OperationsApiImpl.assign_feed_id(new_feed)
393+
db_session.add(new_feed)
394+
db_session.commit()
395+
created_feed = db_session.get(Gtfsrealtimefeed, new_feed.id)
396+
logging.info("Created new GTFS-RT feed with ID: %s", new_feed.stable_id)
397+
payload = OperationGtfsRtFeedImpl.from_orm(created_feed).model_dump()
398+
return JSONResponse(status_code=201, content=jsonable_encoder(payload))

0 commit comments

Comments
 (0)