Skip to content

Commit 88c0528

Browse files
committed
Merge branch 'main' into 293-Psycopg
2 parents 111b29e + 814887b commit 88c0528

File tree

91 files changed

+4312
-1312
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+4312
-1312
lines changed

.github/workflows/api-deployer.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ on:
5656
description: Validator endpoint
5757
required: true
5858
type: string
59+
OPERATIONS_OAUTH2_CLIENT_ID_1PASSWORD:
60+
description: Oauth client id part of the authorization for the operations API
61+
required: true
62+
type: string
5963

6064
env:
6165
python_version: '3.11'
@@ -255,6 +259,11 @@ jobs:
255259
name: feeds_gen
256260
path: api/src/feeds_gen/
257261

262+
- uses: actions/download-artifact@v4
263+
with:
264+
name: feeds_operations_gen
265+
path: functions-python/operations_api/src/feeds_operations_gen/
266+
258267
- name: Build python functions
259268
run: |
260269
scripts/function-python-build.sh --all
@@ -290,11 +299,12 @@ jobs:
290299
env:
291300
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
292301
TRANSITLAND_API_KEY: "op://rbiv7rvkkrsdlpcrz3bmv7nmcu/TansitLand API Key/credential"
302+
OPERATIONS_OAUTH2_CLIENT_ID: ${{ inputs.OPERATIONS_OAUTH2_CLIENT_ID_1PASSWORD }}
293303

294304
- name: Populate Variables
295305
run: |
296306
scripts/replace-variables.sh -in_file infra/backend.conf.rename_me -out_file infra/backend.conf -variables BUCKET_NAME,OBJECT_PREFIX
297-
scripts/replace-variables.sh -in_file infra/vars.tfvars.rename_me -out_file infra/vars.tfvars -variables PROJECT_ID,REGION,ENVIRONMENT,DEPLOYER_SERVICE_ACCOUNT,FEED_API_IMAGE_VERSION,OAUTH2_CLIENT_ID,OAUTH2_CLIENT_SECRET,GLOBAL_RATE_LIMIT_REQ_PER_MINUTE,ARTIFACT_REPO_NAME,VALIDATOR_ENDPOINT,TRANSITLAND_API_KEY
307+
scripts/replace-variables.sh -in_file infra/vars.tfvars.rename_me -out_file infra/vars.tfvars -variables PROJECT_ID,REGION,ENVIRONMENT,DEPLOYER_SERVICE_ACCOUNT,FEED_API_IMAGE_VERSION,OAUTH2_CLIENT_ID,OAUTH2_CLIENT_SECRET,GLOBAL_RATE_LIMIT_REQ_PER_MINUTE,ARTIFACT_REPO_NAME,VALIDATOR_ENDPOINT,TRANSITLAND_API_KEY,OPERATIONS_OAUTH2_CLIENT_ID
298308
299309
- uses: hashicorp/setup-terraform@v3
300310
with:

.github/workflows/api-dev.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ jobs:
2222
GLOBAL_RATE_LIMIT_REQ_PER_MINUTE: ${{ vars.GLOBAL_RATE_LIMIT_REQ_PER_MINUTE }}
2323
TF_APPLY: true
2424
VALIDATOR_ENDPOINT: https://stg-gtfs-validator-web-mbzoxaljzq-ue.a.run.app
25+
OPERATIONS_OAUTH2_CLIENT_ID_1PASSWORD: "op://rbiv7rvkkrsdlpcrz3bmv7nmcu/GCP_RETOOL_OAUTH2_CREDS/username"
2526
secrets:
2627
GCP_MOBILITY_FEEDS_SA_KEY: ${{ secrets.DEV_GCP_MOBILITY_FEEDS_SA_KEY }}
2728
OAUTH2_CLIENT_ID: ${{ secrets.DEV_MOBILITY_FEEDS_OAUTH2_CLIENT_ID}}

.github/workflows/api-prod.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ jobs:
1818
GLOBAL_RATE_LIMIT_REQ_PER_MINUTE: ${{ vars.GLOBAL_RATE_LIMIT_REQ_PER_MINUTE }}
1919
TF_APPLY: true
2020
VALIDATOR_ENDPOINT: https://gtfs-validator-web-mbzoxaljzq-ue.a.run.app
21+
OPERATIONS_OAUTH2_CLIENT_ID_1PASSWORD: "op://rbiv7rvkkrsdlpcrz3bmv7nmcu/GCP_RETOOL_OAUTH2_CREDS/username"
2122
secrets:
2223
GCP_MOBILITY_FEEDS_SA_KEY: ${{ secrets.PROD_GCP_MOBILITY_FEEDS_SA_KEY }}
2324
OAUTH2_CLIENT_ID: ${{ secrets.PROD_MOBILITY_FEEDS_OAUTH2_CLIENT_ID}}

.github/workflows/api-qa.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ jobs:
1818
TF_APPLY: true
1919
GLOBAL_RATE_LIMIT_REQ_PER_MINUTE: ${{ vars.GLOBAL_RATE_LIMIT_REQ_PER_MINUTE }}
2020
VALIDATOR_ENDPOINT: https://stg-gtfs-validator-web-mbzoxaljzq-ue.a.run.app
21+
OPERATIONS_OAUTH2_CLIENT_ID_1PASSWORD: "op://rbiv7rvkkrsdlpcrz3bmv7nmcu/GCP_RETOOL_OAUTH2_CREDS/username"
2122
secrets:
2223
GCP_MOBILITY_FEEDS_SA_KEY: ${{ secrets.QA_GCP_MOBILITY_FEEDS_SA_KEY }}
2324
OAUTH2_CLIENT_ID: ${{ secrets.DEV_MOBILITY_FEEDS_OAUTH2_CLIENT_ID}}

.github/workflows/build-test.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ jobs:
8080
scripts/setup-openapi-generator.sh
8181
scripts/api-gen.sh
8282
83+
- name: Generate Operations API code
84+
run: |
85+
scripts/api-operations-gen.sh
86+
8387
- name: Unit tests - API
8488
shell: bash
8589
run: |
@@ -104,9 +108,16 @@ jobs:
104108
path: api/src/database_gen/
105109
overwrite: true
106110

107-
- name: API generated code
111+
- name: Upload API generated code
108112
uses: actions/upload-artifact@v4
109113
with:
110114
name: feeds_gen
111115
path: api/src/feeds_gen/
116+
overwrite: true
117+
118+
- name: Upload Operations API generated code
119+
uses: actions/upload-artifact@v4
120+
with:
121+
name: feeds_operations_gen
122+
path: functions-python/operations_api/src/feeds_operations_gen/
112123
overwrite: true

api/src/database/database.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,20 @@ def configure_polymorphic_mappers():
4646

4747
def with_db_session(func):
4848
"""
49-
Decorator to handle the session management
50-
:param func: the function to decorate
51-
:return: the decorated function
49+
Decorator to handle the session management for the decorated function.
50+
51+
This decorator ensures that a database session is properly created, committed, rolled back in case of an exception,
52+
and closed. It uses the @contextmanager decorator to manage the lifecycle of the session, providing a clean and
53+
efficient way to handle database interactions.
54+
55+
How it works:
56+
- The decorator checks if a 'db_session' keyword argument is provided to the decorated function.
57+
- If 'db_session' is not provided, it creates a new Database instance and starts a new session using the
58+
start_db_session context manager.
59+
- The context manager ensures that the session is properly committed if no exceptions occur, rolled back if an
60+
exception occurs, and closed in either case.
61+
- The session is then passed to the decorated function as the 'db_session' keyword argument.
62+
- If 'db_session' is already provided, it simply calls the decorated function with the existing session.
5263
"""
5364

5465
def wrapper(*args, **kwargs):
@@ -114,6 +125,13 @@ def is_connected(self):
114125

115126
@contextmanager
116127
def start_db_session(self):
128+
"""
129+
Context manager to start a database session with optional echo.
130+
131+
This method manages the lifecycle of a database session, ensuring that the session is properly created,
132+
committed, rolled back in case of an exception, and closed. The @contextmanager decorator simplifies
133+
resource management by handling the setup and cleanup logic within a single function.
134+
"""
117135
session = self.Session()
118136
try:
119137
yield session

api/src/feeds/impl/feeds_api_impl.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import datetime
22
from typing import List, Union, TypeVar
33

4+
from sqlalchemy import or_
45
from sqlalchemy import select
56
from sqlalchemy.orm import joinedload, Session
67
from sqlalchemy.orm.query import Query
@@ -39,13 +40,13 @@
3940
from feeds_gen.models.gtfs_feed import GtfsFeed
4041
from feeds_gen.models.gtfs_rt_feed import GtfsRTFeed
4142
from middleware.request_context import is_user_email_restricted
42-
from sqlalchemy import or_
4343
from utils.date_utils import valid_iso_date
4444
from utils.location_translation import (
4545
create_location_translation_object,
4646
LocationTranslation,
4747
get_feeds_location_translations,
4848
)
49+
from utils.logger import Logger
4950

5051
T = TypeVar("T", bound="BasicFeed")
5152

@@ -59,9 +60,15 @@ class FeedsApiImpl(BaseFeedsApi):
5960

6061
APIFeedType = Union[BasicFeed, GtfsFeed, GtfsRTFeed]
6162

63+
def __init__(self) -> None:
64+
self.logger = Logger("FeedsApiImpl").get_logger()
65+
6266
@with_db_session
6367
def get_feed(self, id: str, db_session: Session) -> BasicFeed:
6468
"""Get the specified feed from the Mobility Database."""
69+
is_email_restricted = is_user_email_restricted()
70+
self.logger.info(f"User email is restricted: {is_email_restricted}")
71+
6572
feed = (
6673
FeedFilter(stable_id=id, provider__ilike=None, producer_url__ilike=None, status=None)
6774
.filter(Database().get_query_model(db_session, Feed))
@@ -70,8 +77,7 @@ def get_feed(self, id: str, db_session: Session) -> BasicFeed:
7077
or_(
7178
Feed.operational_status == None, # noqa: E711
7279
Feed.operational_status != "wip",
73-
# Allow all feeds to be returned if the user is not restricted
74-
not is_user_email_restricted(),
80+
not is_email_restricted, # Allow all feeds to be returned if the user is not restricted
7581
)
7682
)
7783
.first()
@@ -83,19 +89,30 @@ def get_feed(self, id: str, db_session: Session) -> BasicFeed:
8389

8490
@with_db_session
8591
def get_feeds(
86-
self, limit: int, offset: int, status: str, provider: str, producer_url: str, db_session: Session
92+
self,
93+
limit: int,
94+
offset: int,
95+
status: str,
96+
provider: str,
97+
producer_url: str,
98+
is_official: bool,
99+
db_session: Session,
87100
) -> List[BasicFeed]:
88101
"""Get some (or all) feeds from the Mobility Database."""
102+
is_email_restricted = is_user_email_restricted()
103+
self.logger.info(f"User email is restricted: {is_email_restricted}")
89104
feed_filter = FeedFilter(
90105
status=status, provider__ilike=provider, producer_url__ilike=producer_url, stable_id=None
91106
)
92107
feed_query = feed_filter.filter(Database().get_query_model(db_session, Feed))
108+
if is_official:
109+
feed_query = feed_query.filter(Feed.official)
93110
feed_query = feed_query.filter(Feed.data_type != "gbfs") # Filter out GBFS feeds
94111
feed_query = feed_query.filter(
95112
or_(
96113
Feed.operational_status == None, # noqa: E711
97114
Feed.operational_status != "wip",
98-
not is_user_email_restricted(), # Allow all feeds to be returned if the user is not restricted
115+
not is_email_restricted, # Allow all feeds to be returned if the user is not restricted
99116
)
100117
)
101118
# Results are sorted by provider
@@ -217,6 +234,7 @@ def get_gtfs_feeds(
217234
dataset_latitudes: str,
218235
dataset_longitudes: str,
219236
bounding_filter_method: str,
237+
is_official: bool,
220238
db_session: Session,
221239
) -> List[GtfsFeed]:
222240
"""Get some (or all) GTFS feeds from the Mobility Database."""
@@ -236,14 +254,16 @@ def get_gtfs_feeds(
236254
subquery, dataset_latitudes, dataset_longitudes, bounding_filter_method
237255
).subquery()
238256

257+
is_email_restricted = is_user_email_restricted()
258+
self.logger.info(f"User email is restricted: {is_email_restricted}")
239259
feed_query = (
240260
db_session.query(Gtfsfeed)
241261
.filter(Gtfsfeed.id.in_(subquery))
242262
.filter(
243263
or_(
244264
Gtfsfeed.operational_status == None, # noqa: E711
245265
Gtfsfeed.operational_status != "wip",
246-
not is_user_email_restricted(), # Allow all feeds to be returned if the user is not restricted
266+
not is_email_restricted, # Allow all feeds to be returned if the user is not restricted
247267
)
248268
)
249269
.options(
@@ -253,9 +273,10 @@ def get_gtfs_feeds(
253273
*BasicFeedImpl.get_joinedload_options(),
254274
)
255275
.order_by(Gtfsfeed.provider, Gtfsfeed.stable_id)
256-
.limit(limit)
257-
.offset(offset)
258276
)
277+
if is_official:
278+
feed_query = feed_query.filter(Feed.official)
279+
feed_query = feed_query.limit(limit).offset(offset)
259280
return self._get_response(feed_query, GtfsFeedImpl, db_session)
260281

261282
@with_db_session
@@ -303,6 +324,7 @@ def get_gtfs_rt_feeds(
303324
country_code: str,
304325
subdivision_name: str,
305326
municipality: str,
327+
is_official: bool,
306328
db_session: Session,
307329
) -> List[GtfsRTFeed]:
308330
"""Get some (or all) GTFS Realtime feeds from the Mobility Database."""
@@ -350,9 +372,10 @@ def get_gtfs_rt_feeds(
350372
*BasicFeedImpl.get_joinedload_options(),
351373
)
352374
.order_by(Gtfsrealtimefeed.provider, Gtfsrealtimefeed.stable_id)
353-
.limit(limit)
354-
.offset(offset)
355375
)
376+
if is_official:
377+
feed_query = feed_query.filter(Feed.official)
378+
feed_query = feed_query.limit(limit).offset(offset)
356379
return self._get_response(feed_query, GtfsRTFeedImpl, db_session)
357380

358381
@staticmethod

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ class Config:
2323
def from_orm(cls, feed: Feed | None, _=None) -> BasicFeed | None:
2424
if not feed:
2525
return None
26+
latest_official_status = None
27+
if len(feed.officialstatushistories) > 0:
28+
latest_official_status = max(feed.officialstatushistories, key=lambda x: x.timestamp).is_official
2629
return cls(
2730
id=feed.stable_id,
2831
data_type=feed.data_type,
2932
status=feed.status,
33+
official=latest_official_status,
3034
created_at=feed.created_at,
3135
external_ids=sorted(
3236
[ExternalIdImpl.from_orm(item) for item in feed.externalids], key=lambda x: x.external_id
@@ -48,7 +52,12 @@ def from_orm(cls, feed: Feed | None, _=None) -> BasicFeed | None:
4852
@staticmethod
4953
def get_joinedload_options() -> [_AbstractLoad]:
5054
"""Returns common joinedload options for feeds queries."""
51-
return [joinedload(Feed.locations), joinedload(Feed.externalids), joinedload(Feed.redirectingids)]
55+
return [
56+
joinedload(Feed.locations),
57+
joinedload(Feed.externalids),
58+
joinedload(Feed.redirectingids),
59+
joinedload(Feed.officialstatushistories),
60+
]
5261

5362

5463
class BasicFeedImpl(BaseFeedImpl, BasicFeed):

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def from_orm_gtfs(cls, feed_search_row):
2525
external_ids=feed_search_row.external_ids,
2626
provider=feed_search_row.provider,
2727
feed_name=feed_search_row.feed_name,
28+
official=feed_search_row.official,
2829
note=feed_search_row.note,
2930
feed_contact_email=feed_search_row.feed_contact_email,
3031
source_info=SourceInfo(
@@ -58,6 +59,7 @@ def from_orm_gtfs_rt(cls, feed_search_row):
5859
external_ids=feed_search_row.external_ids,
5960
provider=feed_search_row.provider,
6061
feed_name=feed_search_row.feed_name,
62+
official=feed_search_row.official,
6163
note=feed_search_row.note,
6264
feed_contact_email=feed_search_row.feed_contact_email,
6365
source_info=SourceInfo(

0 commit comments

Comments
 (0)