Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions api/.openapi-generator/FILES
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
src/feeds/impl/__init__.py
src/feeds_gen/apis/__init__.py
src/feeds_gen/apis/beta_api.py
src/feeds_gen/apis/beta_api_base.py
src/feeds_gen/apis/datasets_api.py
src/feeds_gen/apis/datasets_api_base.py
src/feeds_gen/apis/feeds_api.py
Expand Down
62 changes: 62 additions & 0 deletions api/src/scripts/populate_db_test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
Notice,
Feature,
License,
LicenseTag,
LicenseTagGroup,
t_feedsearch,
Location,
Officialstatushistory,
Expand Down Expand Up @@ -224,6 +226,66 @@ def populate_test_datasets(self, filepath, db_session: "Session"):
license_obj.rules.append(rule_obj)
db_session.commit()

# License tag groups (optional section to seed group metadata used by license_tags)
if "license_tag_groups" in data:
for group in data["license_tag_groups"]:
group_id = group.get("id")
if not group_id:
continue
existing_group = db_session.get(LicenseTagGroup, group_id)
if existing_group:
continue
db_session.add(
LicenseTagGroup(
id=group_id,
short_name=group.get("short_name"),
description=group.get("description") or group_id,
)
)
db_session.commit()

# License tags (optional section to seed tag metadata)
if "license_tags" in data:
for tag in data["license_tags"]:
tag_id = tag.get("id")
if not tag_id:
continue
existing_tag = db_session.get(LicenseTag, tag_id)
if existing_tag:
continue
db_session.add(
LicenseTag(
id=tag_id,
group=tag.get("group"),
tag=tag.get("tag"),
url=tag.get("url"),
description=tag.get("description"),
)
)
db_session.commit()

# License tag associations: attach tags to licenses via the many-to-many relationship
if "license_license_tags" in data:
for lt in data["license_license_tags"]:
license_id = lt.get("license_id")
tag_id = lt.get("tag_id")
if not license_id or not tag_id:
continue
license_obj = db_session.get(License, license_id)
if not license_obj:
self.logger.error(
f"No license found with id: {license_id};"
f" skipping license_license_tag association for tag {tag_id}"
)
continue
tag_obj = db_session.get(LicenseTag, tag_id)
if not tag_obj:
self.logger.error(f"No license tag found with id: {tag_id}; skipping")
continue
if tag_obj not in license_obj.tags:
license_obj.tags.append(tag_obj)
db_session.commit()

# GBFS version
if "gbfs_versions" in data:
for version in data["gbfs_versions"]:
Expand Down
3 changes: 3 additions & 0 deletions api/src/shared/db_models/basic_feed_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ def from_orm(cls, feed: Feed | None) -> BasicFeed | None:
return None
# Determine license_is_spdx from the related License ORM if available
license_is_spdx = None
license_tags = None
if getattr(feed, "license", None) is not None:
license_is_spdx = feed.license.is_spdx
license_tags = sorted([tag.id for tag in getattr(feed.license, "tags", [])]) or None

return cls(
id=feed.stable_id,
Expand All @@ -43,6 +45,7 @@ def from_orm(cls, feed: Feed | None) -> BasicFeed | None:
license_id=feed.license_id,
license_is_spdx=license_is_spdx,
license_notes=feed.license_notes,
license_tags=license_tags,
),
redirects=sorted([RedirectImpl.from_orm(item) for item in feed.redirectingids], key=lambda x: x.target_id),
)
Expand Down
1 change: 1 addition & 0 deletions api/src/shared/db_models/license_base_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ def from_orm(cls, license_orm: Optional[LicenseOrm]) -> Optional[LicenseBase]:
description=license_orm.description,
created_at=license_orm.created_at,
updated_at=license_orm.updated_at,
license_tags=sorted([tag.id for tag in getattr(license_orm, "tags", [])]) or None,
)
39 changes: 39 additions & 0 deletions api/tests/integration/test_data/extra_test_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,45 @@
}
],

"license_tag_groups": [
{
"id": "family",
"short_name": "Family",
"description": "License family taxonomy"
},
{
"id": "license",
"short_name": "License",
"description": "License type taxonomy"
}
],

"license_tags": [
{
"id": "family:ODC",
"group": "family",
"tag": "ODC",
"description": "Open Data Commons family"
},
{
"id": "license:open-data-commons",
"group": "license",
"tag": "open-data-commons",
"description": "Open Data Commons license"
}
],

"license_license_tags": [
{
"license_id": "license-1",
"tag_id": "family:ODC"
},
{
"license_id": "license-1",
"tag_id": "license:open-data-commons"
}
],

"feeds": [
{
"id": "mdb-60",
Expand Down
20 changes: 15 additions & 5 deletions api/tests/integration/test_feeds_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -947,18 +947,23 @@ def test_gbfs_feed_id_get(client: TestClient, values):


@pytest.mark.parametrize(
"feed_id, expected_license_id, expected_is_spdx, expected_license_notes",
"feed_id, expected_license_id, expected_is_spdx, expected_license_notes, expected_license_tags",
[
("mdb-70", "license-1", True, "Notes for license-1"),
("mdb-80", "license-2", False, None),
("mdb-70", "license-1", True, "Notes for license-1", ["family:ODC", "license:open-data-commons"]),
("mdb-80", "license-2", False, None, None),
],
)
def test_feeds_have_expected_license_info(
client: TestClient, feed_id: str, expected_license_id: str, expected_is_spdx: bool, expected_license_notes: str
client: TestClient,
feed_id: str,
expected_license_id: str,
expected_is_spdx: bool,
expected_license_notes: str,
expected_license_tags: list,
):
"""
Verify that specified feeds have the expected license id,
license_is_spdx and license_notes from the test fixture.
license_is_spdx, license_notes, and license_tags from the test fixture.
"""
response = client.request(
"GET",
Expand All @@ -974,3 +979,8 @@ def test_feeds_have_expected_license_info(
assert body["source_info"]["license_is_spdx"] is expected_is_spdx
# Check license_notes (may be None)
assert body["source_info"].get("license_notes") == expected_license_notes
# Check license_tags (may be None if no tags)
if expected_license_tags is None:
assert body["source_info"].get("license_tags") is None
else:
assert sorted(body["source_info"].get("license_tags", [])) == sorted(expected_license_tags)
10 changes: 9 additions & 1 deletion api/tests/integration/test_licenses_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


@pytest.mark.parametrize(
"license_id, expected_is_spdx, expected_name, expected_url, expected_description, expected_rules",
"license_id, expected_is_spdx, expected_name, expected_url, expected_description, expected_rules, expected_tags",
[
(
"license-1",
Expand All @@ -28,6 +28,7 @@
"type": "condition",
},
],
["family:ODC", "license:open-data-commons"],
),
(
"license-2",
Expand All @@ -49,6 +50,7 @@
"type": "limitation",
},
],
None,
),
],
)
Expand All @@ -60,6 +62,7 @@ def test_get_license_by_id(
expected_url: str,
expected_description: str,
expected_rules: list,
expected_tags: list,
):
"""GET /v1/licenses/{id} returns the expected license fields for known test licenses."""
response = client.request("GET", f"/v1/licenses/{license_id}", headers=authHeaders)
Expand All @@ -74,6 +77,11 @@ def test_get_license_by_id(
actual_rules = {rule["name"]: rule for rule in body.get("license_rules", [])}
expected_rule_map = {rule["name"]: rule for rule in expected_rules}
assert actual_rules == expected_rule_map
# Verify license_tags
if expected_tags is None:
assert body.get("license_tags") is None
else:
assert sorted(body.get("license_tags", [])) == sorted(expected_tags)


def test_get_licenses_list_contains_test_licenses(client: TestClient):
Expand Down
18 changes: 16 additions & 2 deletions docs/DatabaseCatalogAPI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ paths:
description: Get GBFS feeds from the Mobility Database.
tags:
- "feeds"
- "beta"
Copy link
Copy Markdown
Member

@davidgamez davidgamez Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed beta endpoint tags as they are not beta anymore.

operationId: getGbfsFeeds
parameters:
- $ref: "#/components/parameters/limit_query_param_gbfs_feeds_endpoint"
Expand Down Expand Up @@ -175,7 +174,6 @@ paths:
description: Get the specified GTFS feed from the Mobility Database. Once a week, we check if the latest dataset has been updated and, if so, we update it in our system accordingly.
tags:
- "feeds"
- "beta"
operationId: getGtfsFeed

security:
Expand Down Expand Up @@ -1088,6 +1086,14 @@ components:
description: Notes concerning the relation between the feed and the license.
type: string
example: Detected locale/jurisdiction port 'nl'. SPDX does not list ported CC licenses; using canonical ID.
license_tags:
description: List of taxonomy tags associated with the feed's license.
type: array
items:
type: string
example:
- "family:ODC"
- "license:open-data-commons"

Locations:
type: array
Expand Down Expand Up @@ -1345,6 +1351,14 @@ components:
type: string
example: 2023-07-10T22:06:00Z
format: date-time
license_tags:
description: List of taxonomy tags associated with the license.
type: array
items:
type: string
example:
- "family:ODC"
- "license:open-data-commons"

LicenseWithRules:
allOf:
Expand Down
20 changes: 20 additions & 0 deletions docs/OperationsAPI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,14 @@ components:
description: Notes concerning the relation between the feed and the license.
type: string
example: Detected locale/jurisdiction port 'nl'. SPDX does not list ported CC licenses; using canonical ID.
license_tags:
description: List of taxonomy tags associated with the feed's license.
type: array
items:
type: string
example:
- "family:ODC"
- "license:open-data-commons"
Locations:
type: array
items:
Expand Down Expand Up @@ -1268,6 +1276,14 @@ components:
type: string
example: 2023-07-10T22:06:00Z
format: date-time
license_tags:
description: List of taxonomy tags associated with the license.
type: array
items:
type: string
example:
- "family:ODC"
- "license:open-data-commons"
LicenseWithRules:
allOf:
- $ref: "#/components/schemas/LicenseBase"
Expand Down Expand Up @@ -1478,6 +1494,7 @@ components:




* vp - vehicle positions
* tu - trip updates
* sa - service alerts
Expand Down Expand Up @@ -1599,6 +1616,7 @@ components:




* vp - vehicle positions
* tu - trip updates
* sa - service alerts
Expand Down Expand Up @@ -1677,6 +1695,7 @@ components:




* `active` Feed should be used in public trip planners.
* `deprecated` Feed is explicitly deprecated and should not be used in public trip planners.
* `inactive` Feed hasn't been recently updated and should be used at risk of providing outdated information.
Expand All @@ -1697,6 +1716,7 @@ components:




* `gtfs` GTFS feed.
* `gtfs_rt` GTFS-RT feed.
* `gbfs` GBFS feed.
Expand Down
16 changes: 16 additions & 0 deletions web-app/src/app/services/feeds/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,14 @@ export interface components {
* @example Detected locale/jurisdiction port 'nl'. SPDX does not list ported CC licenses; using canonical ID.
*/
license_notes?: string;
/**
* @description List of taxonomy tags associated with the feed's license.
* @example [
* "family:ODC",
* "license:open-data-commons"
* ]
*/
license_tags?: string[];
};
Locations: Array<components['schemas']['Location']>;
Location: {
Expand Down Expand Up @@ -848,6 +856,14 @@ export interface components {
* @example "2023-07-10T22:06:00.000Z"
*/
updated_at?: string;
/**
* @description List of taxonomy tags associated with the license.
* @example [
* "family:ODC",
* "license:open-data-commons"
* ]
*/
license_tags?: string[];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[out of scope for this PR] are we dropping the web app folder from this repo now that we have the beautiful SSR repo?

Copy link
Copy Markdown
Member

@davidgamez davidgamez Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be done with the JS here soon! We are currently monitoring the SSR deployment. cc: @Alessandro100

};
LicenseWithRules: components['schemas']['LicenseBase'] & {
license_rules?: Array<components['schemas']['LicenseRule']>;
Expand Down
Loading