Skip to content

Commit 46143a2

Browse files
authored
feat: added GBFS endpoints (#1103)
1 parent 24ceb1d commit 46143a2

35 files changed

+1059
-311
lines changed

api/.openapi-generator/FILES

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
src/feeds/impl/__init__.py
22
src/feeds_gen/apis/__init__.py
3+
src/feeds_gen/apis/beta_api.py
4+
src/feeds_gen/apis/beta_api_base.py
35
src/feeds_gen/apis/datasets_api.py
46
src/feeds_gen/apis/datasets_api_base.py
57
src/feeds_gen/apis/feeds_api.py
@@ -15,6 +17,11 @@ src/feeds_gen/models/basic_feed.py
1517
src/feeds_gen/models/bounding_box.py
1618
src/feeds_gen/models/external_id.py
1719
src/feeds_gen/models/extra_models.py
20+
src/feeds_gen/models/feed.py
21+
src/feeds_gen/models/gbfs_endpoint.py
22+
src/feeds_gen/models/gbfs_feed.py
23+
src/feeds_gen/models/gbfs_validation_report.py
24+
src/feeds_gen/models/gbfs_version.py
1825
src/feeds_gen/models/gtfs_dataset.py
1926
src/feeds_gen/models/gtfs_feed.py
2027
src/feeds_gen/models/gtfs_rt_feed.py

api/src/feeds/impl/feeds_api_impl.py

Lines changed: 82 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,39 @@
66
from sqlalchemy.orm import joinedload, Session
77
from sqlalchemy.orm.query import Query
88

9+
from feeds.impl.datasets_api_impl import DatasetsApiImpl
10+
from feeds.impl.error_handling import raise_http_error, raise_http_validation_error, convert_exception
11+
from feeds.impl.models.entity_type_enum import EntityType
12+
from feeds.impl.models.feed_impl import FeedImpl
13+
from feeds.impl.models.gbfs_feed_impl import GbfsFeedImpl
14+
from feeds.impl.models.gtfs_feed_impl import GtfsFeedImpl
15+
from feeds.impl.models.gtfs_rt_feed_impl import GtfsRTFeedImpl
16+
from feeds_gen.apis.feeds_api_base import BaseFeedsApi
17+
from feeds_gen.models.feed import Feed
18+
from feeds_gen.models.gbfs_feed import GbfsFeed
19+
from feeds_gen.models.gtfs_dataset import GtfsDataset
20+
from feeds_gen.models.gtfs_feed import GtfsFeed
21+
from feeds_gen.models.gtfs_rt_feed import GtfsRTFeed
22+
from middleware.request_context import is_user_email_restricted
923
from shared.common.db_utils import (
1024
get_gtfs_feeds_query,
1125
get_gtfs_rt_feeds_query,
1226
get_joinedload_options,
1327
add_official_filter,
28+
get_gbfs_feeds_query,
1429
)
30+
from shared.common.error_handling import (
31+
invalid_date_message,
32+
feed_not_found,
33+
gtfs_feed_not_found,
34+
gtfs_rt_feed_not_found,
35+
InternalHTTPException,
36+
gbfs_feed_not_found,
37+
)
38+
from shared.common.logging_utils import Logger
1539
from shared.database.database import Database, with_db_session
1640
from shared.database_gen.sqlacodegen_models import (
17-
Feed,
41+
Feed as FeedOrm,
1842
Gtfsdataset,
1943
Gtfsfeed,
2044
Gtfsrealtimefeed,
@@ -25,29 +49,9 @@
2549
from shared.feed_filters.gtfs_dataset_filter import GtfsDatasetFilter
2650
from shared.feed_filters.gtfs_feed_filter import LocationFilter
2751
from shared.feed_filters.gtfs_rt_feed_filter import GtfsRtFeedFilter, EntityTypeFilter
28-
from feeds.impl.datasets_api_impl import DatasetsApiImpl
29-
from shared.common.error_handling import (
30-
invalid_date_message,
31-
feed_not_found,
32-
gtfs_feed_not_found,
33-
gtfs_rt_feed_not_found,
34-
InternalHTTPException,
35-
)
36-
from feeds.impl.models.basic_feed_impl import BasicFeedImpl
37-
from feeds.impl.models.entity_type_enum import EntityType
38-
from feeds.impl.models.gtfs_feed_impl import GtfsFeedImpl
39-
from feeds.impl.models.gtfs_rt_feed_impl import GtfsRTFeedImpl
40-
from feeds_gen.apis.feeds_api_base import BaseFeedsApi
41-
from feeds_gen.models.basic_feed import BasicFeed
42-
from feeds_gen.models.gtfs_dataset import GtfsDataset
43-
from feeds_gen.models.gtfs_feed import GtfsFeed
44-
from feeds_gen.models.gtfs_rt_feed import GtfsRTFeed
45-
from feeds.impl.error_handling import raise_http_error, raise_http_validation_error, convert_exception
46-
from middleware.request_context import is_user_email_restricted
4752
from utils.date_utils import valid_iso_date
48-
from shared.common.logging_utils import Logger
4953

50-
T = TypeVar("T", bound="BasicFeed")
54+
T = TypeVar("T", bound="Feed")
5155

5256

5357
class FeedsApiImpl(BaseFeedsApi):
@@ -57,31 +61,30 @@ class FeedsApiImpl(BaseFeedsApi):
5761
If a method is left blank the associated endpoint will return a 500 HTTP response.
5862
"""
5963

60-
APIFeedType = Union[BasicFeed, GtfsFeed, GtfsRTFeed]
64+
APIFeedType = Union[FeedOrm, GtfsFeed, GtfsRTFeed]
6165

6266
def __init__(self) -> None:
6367
self.logger = Logger("FeedsApiImpl").get_logger()
6468

6569
@with_db_session
66-
def get_feed(self, id: str, db_session: Session) -> BasicFeed:
70+
def get_feed(self, id: str, db_session: Session) -> Feed:
6771
"""Get the specified feed from the Mobility Database."""
6872
is_email_restricted = is_user_email_restricted()
6973
self.logger.debug(f"User email is restricted: {is_email_restricted}")
7074

7175
feed = (
7276
FeedFilter(stable_id=id, provider__ilike=None, producer_url__ilike=None, status=None)
73-
.filter(Database().get_query_model(db_session, Feed))
74-
.filter(Feed.data_type != "gbfs") # Filter out GBFS feeds
77+
.filter(Database().get_query_model(db_session, FeedOrm))
7578
.filter(
7679
or_(
77-
Feed.operational_status == "published",
80+
FeedOrm.operational_status == "published",
7881
not is_email_restricted, # Allow all feeds to be returned if the user is not restricted
7982
)
8083
)
8184
.first()
8285
)
8386
if feed:
84-
return BasicFeedImpl.from_orm(feed)
87+
return FeedImpl.from_orm(feed)
8588
else:
8689
raise_http_error(404, feed_not_found.format(id))
8790

@@ -95,32 +98,31 @@ def get_feeds(
9598
producer_url: str,
9699
is_official: bool,
97100
db_session: Session,
98-
) -> List[BasicFeed]:
101+
) -> List[Feed]:
99102
"""Get some (or all) feeds from the Mobility Database."""
100103
is_email_restricted = is_user_email_restricted()
101104
self.logger.debug(f"User email is restricted: {is_email_restricted}")
102105
feed_filter = FeedFilter(
103106
status=status, provider__ilike=provider, producer_url__ilike=producer_url, stable_id=None
104107
)
105-
feed_query = feed_filter.filter(Database().get_query_model(db_session, Feed))
108+
feed_query = feed_filter.filter(Database().get_query_model(db_session, FeedOrm))
106109
feed_query = add_official_filter(feed_query, is_official)
107-
feed_query = feed_query.filter(Feed.data_type != "gbfs") # Filter out GBFS feeds
108110
feed_query = feed_query.filter(
109111
or_(
110-
Feed.operational_status == "published",
112+
FeedOrm.operational_status == "published",
111113
not is_email_restricted, # Allow all feeds to be returned if the user is not restricted
112114
)
113115
)
114116
# Results are sorted by provider
115-
feed_query = feed_query.order_by(Feed.provider, Feed.stable_id)
117+
feed_query = feed_query.order_by(FeedOrm.provider, FeedOrm.stable_id)
116118
feed_query = feed_query.options(*get_joinedload_options())
117119
if limit is not None:
118120
feed_query = feed_query.limit(limit)
119121
if offset is not None:
120122
feed_query = feed_query.offset(offset)
121123

122124
results = feed_query.all()
123-
return [BasicFeedImpl.from_orm(feed) for feed in results]
125+
return [FeedImpl.from_orm(feed) for feed in results]
124126

125127
@with_db_session
126128
def get_gtfs_feed(self, id: str, db_session: Session) -> GtfsFeed:
@@ -163,7 +165,7 @@ def get_gtfs_feed_datasets(
163165
feed = self._get_gtfs_feed(gtfs_feed_id, db_session, include_options_for_joinedload=False)
164166

165167
if not feed:
166-
raise_http_error(404, f"Feed with id {gtfs_feed_id} not found")
168+
raise_http_error(404, f"FeedOrm with id {gtfs_feed_id} not found")
167169

168170
# Replace Z with +00:00 to make the datetime object timezone aware
169171
# Due to https://github.com/python/cpython/issues/80010, once migrate to Python 3.11, we can use fromisoformat
@@ -174,7 +176,7 @@ def get_gtfs_feed_datasets(
174176
downloaded_at__gte=(
175177
datetime.fromisoformat(downloaded_after.replace("Z", "+00:00")) if downloaded_after else None
176178
),
177-
).filter(DatasetsApiImpl.create_dataset_query().filter(Feed.stable_id == gtfs_feed_id))
179+
).filter(DatasetsApiImpl.create_dataset_query().filter(FeedOrm.stable_id == gtfs_feed_id))
178180

179181
if latest:
180182
query = query.filter(Gtfsdataset.latest)
@@ -352,3 +354,47 @@ def get_gtfs_feed_gtfs_rt_feeds(self, id: str, db_session: Session) -> List[Gtfs
352354
return [GtfsRTFeedImpl.from_orm(gtfs_rt_feed) for gtfs_rt_feed in feed.gtfs_rt_feeds]
353355
else:
354356
raise_http_error(404, gtfs_feed_not_found.format(id))
357+
358+
@with_db_session
359+
def get_gbfs_feed(
360+
self,
361+
id: str,
362+
db_session: Session,
363+
) -> GbfsFeed:
364+
"""Get the specified GBFS feed from the Mobility Database."""
365+
result = get_gbfs_feeds_query(db_session, stable_id=id).one_or_none()
366+
if result:
367+
return GbfsFeedImpl.from_orm(result)
368+
else:
369+
raise_http_error(404, gbfs_feed_not_found.format(id))
370+
371+
@with_db_session
372+
def get_gbfs_feeds(
373+
self,
374+
limit: int,
375+
offset: int,
376+
provider: str,
377+
producer_url: str,
378+
country_code: str,
379+
subdivision_name: str,
380+
municipality: str,
381+
system_id: str,
382+
version: str,
383+
db_session: Session,
384+
) -> List[GbfsFeed]:
385+
query = get_gbfs_feeds_query(
386+
db_session=db_session,
387+
provider=provider,
388+
producer_url=producer_url,
389+
country_code=country_code,
390+
subdivision_name=subdivision_name,
391+
municipality=municipality,
392+
system_id=system_id,
393+
version=version,
394+
)
395+
if limit:
396+
query = query.limit(limit)
397+
if offset:
398+
query = query.offset(offset)
399+
results = query.all()
400+
return [GbfsFeedImpl.from_orm(feed) for feed in results]

api/src/feeds/impl/models/basic_feed_impl.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,11 @@ def from_orm(cls, feed: Feed | None) -> BasicFeed | None:
2323
return cls(
2424
id=feed.stable_id,
2525
data_type=feed.data_type,
26-
status=feed.status,
27-
official=feed.official,
28-
official_updated_at=feed.official_updated_at,
2926
created_at=feed.created_at,
3027
external_ids=sorted(
3128
[ExternalIdImpl.from_orm(item) for item in feed.externalids], key=lambda x: x.external_id
3229
),
3330
provider=feed.provider,
34-
feed_name=feed.feed_name,
35-
note=feed.note,
3631
feed_contact_email=feed.feed_contact_email,
3732
source_info=SourceInfo(
3833
producer_url=feed.producer_url,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from feeds.impl.models.basic_feed_impl import BaseFeedImpl
2+
from feeds_gen.models.feed import Feed
3+
from shared.database_gen.sqlacodegen_models import Feed as FeedOrm
4+
5+
6+
class FeedImpl(BaseFeedImpl, Feed):
7+
"""Base implementation of the feeds models.
8+
This class converts a SQLAlchemy row DB object with common feed fields to a Pydantic model.
9+
"""
10+
11+
class Config:
12+
"""Pydantic configuration.
13+
Enabling `from_attributes` method to create a model instance from a SQLAlchemy row object."""
14+
15+
from_attributes = True
16+
17+
@classmethod
18+
def from_orm(cls, feed_orm: FeedOrm | None) -> Feed | None:
19+
feed: Feed = super().from_orm(feed_orm)
20+
if not feed:
21+
return None
22+
feed.status = feed_orm.status
23+
feed.official = feed_orm.official
24+
feed.official_updated_at = feed_orm.official_updated_at
25+
feed.feed_name = feed_orm.feed_name
26+
feed.note = feed_orm.note
27+
return feed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from feeds_gen.models.gbfs_endpoint import GbfsEndpoint
2+
from shared.database_gen.sqlacodegen_models import Gbfsendpoint as GbfsEndpointOrm
3+
4+
5+
class GbfsEndpointImpl(GbfsEndpoint):
6+
"""Implementation of the `GtfsFeed` model.
7+
This class converts a SQLAlchemy row DB object to a Pydantic model.
8+
"""
9+
10+
class Config:
11+
"""Pydantic configuration.
12+
Enabling `from_attributes` method to create a model instance from a SQLAlchemy row object."""
13+
14+
from_attributes = True
15+
16+
@classmethod
17+
def from_orm(cls, endpoint: GbfsEndpointOrm | None) -> GbfsEndpoint | None:
18+
if not endpoint:
19+
return None
20+
return cls(name=endpoint.name, url=endpoint.url, language=endpoint.language, is_feature=endpoint.is_feature)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from feeds.impl.models.feed_impl import FeedImpl
2+
from feeds.impl.models.gbfs_version_impl import GbfsVersionImpl
3+
from shared.database_gen.sqlacodegen_models import Gbfsfeed as GbfsFeedOrm
4+
from feeds.impl.models.location_impl import LocationImpl
5+
from feeds_gen.models.gbfs_feed import GbfsFeed
6+
7+
8+
class GbfsFeedImpl(FeedImpl, GbfsFeed):
9+
"""Implementation of the `GtfsFeed` model.
10+
This class converts a SQLAlchemy row DB object to a Pydantic model.
11+
"""
12+
13+
class Config:
14+
"""Pydantic configuration.
15+
Enabling `from_attributes` method to create a model instance from a SQLAlchemy row object."""
16+
17+
from_attributes = True
18+
19+
@classmethod
20+
def from_orm(cls, feed: GbfsFeedOrm | None) -> GbfsFeed | None:
21+
gbfs_feed: GbfsFeed = super().from_orm(feed)
22+
if not gbfs_feed:
23+
return None
24+
gbfs_feed.locations = [LocationImpl.from_orm(item) for item in feed.locations] if feed.locations else []
25+
gbfs_feed.system_id = feed.system_id
26+
gbfs_feed.provider_url = feed.operator_url
27+
gbfs_feed.versions = (
28+
[GbfsVersionImpl.from_orm(item) for item in feed.gbfsversions if item is not None]
29+
if feed.gbfsversions
30+
else []
31+
)
32+
return gbfs_feed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from feeds_gen.models.gbfs_version import GbfsValidationReport
2+
from shared.database_gen.sqlacodegen_models import Gbfsvalidationreport as GbfsValidationReportOrm
3+
4+
5+
class GbfsValidationReportImpl(GbfsValidationReport):
6+
"""Implementation of the `GtfsFeed` model.
7+
This class converts a SQLAlchemy row DB object to a Pydantic model.
8+
"""
9+
10+
class Config:
11+
"""Pydantic configuration.
12+
Enabling `from_attributes` method to create a model instance from a SQLAlchemy row object."""
13+
14+
from_attributes = True
15+
16+
@classmethod
17+
def from_orm(cls, validation_report: GbfsValidationReportOrm | None) -> GbfsValidationReport | None:
18+
if not validation_report:
19+
return None
20+
return cls(
21+
validated_at=validation_report.validated_at,
22+
total_error=validation_report.total_errors_count,
23+
report_summary_url=validation_report.report_summary_url,
24+
validator_version=validation_report.validator_version,
25+
)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from feeds.impl.models.gbfs_endpoint_impl import GbfsEndpointImpl
2+
from feeds.impl.models.gbfs_validation_report_impl import GbfsValidationReportImpl
3+
from feeds_gen.models.gbfs_version import GbfsVersion
4+
from shared.database_gen.sqlacodegen_models import Gbfsversion as GbfsVersionOrm
5+
6+
7+
class GbfsVersionImpl(GbfsVersion):
8+
"""Implementation of the `GtfsFeed` model.
9+
This class converts a SQLAlchemy row DB object to a Pydantic model.
10+
"""
11+
12+
class Config:
13+
"""Pydantic configuration.
14+
Enabling `from_attributes` method to create a model instance from a SQLAlchemy row object."""
15+
16+
from_attributes = True
17+
18+
@classmethod
19+
def from_orm(cls, version: GbfsVersionOrm | None) -> GbfsVersion | None:
20+
if not version:
21+
return None
22+
latest_report = (
23+
GbfsValidationReportImpl.from_orm(version.gbfsvalidationreports[0])
24+
if len(version.gbfsvalidationreports) > 0
25+
else None
26+
)
27+
return cls(
28+
version=version.version,
29+
created_at=version.created_at,
30+
last_updated_at=latest_report.validated_at if latest_report else None,
31+
latest=version.latest,
32+
endpoints=[GbfsEndpointImpl.from_orm(item) for item in version.gbfsendpoints]
33+
if version.gbfsendpoints
34+
else [],
35+
latest_validation_report=latest_report,
36+
)

0 commit comments

Comments
 (0)