Skip to content

Commit b9805df

Browse files
authored
Merge pull request #521 from Police-Data-Accessibility-Project/mc_get_data_source_by_id
Add `data-sources/:id` `GET` endpoint
2 parents 35875dd + 94c32e3 commit b9805df

File tree

11 files changed

+168
-115
lines changed

11 files changed

+168
-115
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from sqlalchemy import Select, select, and_
2+
from sqlalchemy.orm import selectinload
3+
4+
from src.db.models.impl.flag.url_validated.enums import URLType
5+
from src.db.models.impl.flag.url_validated.sqlalchemy import FlagURLValidated
6+
from src.db.models.impl.link.batch_url.sqlalchemy import LinkBatchURL
7+
from src.db.models.impl.url.core.sqlalchemy import URL
8+
from src.db.models.impl.url.optional_ds_metadata.sqlalchemy import URLOptionalDataSourceMetadata
9+
from src.db.models.impl.url.record_type.sqlalchemy import URLRecordType
10+
11+
12+
def build_data_source_get_query() -> Select:
13+
return (
14+
select(
15+
URL,
16+
URL.id,
17+
URL.url,
18+
19+
# Required Attributes
20+
URL.name,
21+
URLRecordType.record_type,
22+
23+
# Optional Attributes
24+
URL.description,
25+
LinkBatchURL.batch_id,
26+
URLOptionalDataSourceMetadata.record_formats,
27+
URLOptionalDataSourceMetadata.data_portal_type,
28+
URLOptionalDataSourceMetadata.supplying_entity,
29+
URLOptionalDataSourceMetadata.coverage_start,
30+
URLOptionalDataSourceMetadata.coverage_end,
31+
URLOptionalDataSourceMetadata.agency_supplied,
32+
URLOptionalDataSourceMetadata.agency_aggregation,
33+
URLOptionalDataSourceMetadata.agency_described_not_in_database,
34+
URLOptionalDataSourceMetadata.agency_originated,
35+
URLOptionalDataSourceMetadata.update_method,
36+
URLOptionalDataSourceMetadata.readme_url,
37+
URLOptionalDataSourceMetadata.originating_entity,
38+
URLOptionalDataSourceMetadata.retention_schedule,
39+
URLOptionalDataSourceMetadata.scraper_url,
40+
URLOptionalDataSourceMetadata.submission_notes,
41+
URLOptionalDataSourceMetadata.access_notes,
42+
URLOptionalDataSourceMetadata.access_types
43+
)
44+
.join(
45+
URLRecordType,
46+
URLRecordType.url_id == URL.id
47+
)
48+
.join(
49+
FlagURLValidated,
50+
and_(
51+
FlagURLValidated.url_id == URL.id,
52+
FlagURLValidated.type == URLType.DATA_SOURCE
53+
)
54+
)
55+
.outerjoin(
56+
LinkBatchURL,
57+
LinkBatchURL.url_id == URL.id
58+
)
59+
.outerjoin(
60+
URLOptionalDataSourceMetadata,
61+
URLOptionalDataSourceMetadata.url_id == URL.id
62+
)
63+
.options(
64+
selectinload(URL.confirmed_agencies),
65+
)
66+
)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from sqlalchemy import RowMapping
2+
3+
from src.api.endpoints.data_source.get.response import DataSourceGetResponse
4+
from src.db.models.impl.link.batch_url.sqlalchemy import LinkBatchURL
5+
from src.db.models.impl.url.core.sqlalchemy import URL
6+
from src.db.models.impl.url.optional_ds_metadata.sqlalchemy import URLOptionalDataSourceMetadata
7+
from src.db.models.impl.url.record_type.sqlalchemy import URLRecordType
8+
9+
10+
def process_data_source_get_mapping(
11+
mapping: RowMapping
12+
) -> DataSourceGetResponse:
13+
url: URL = mapping[URL]
14+
15+
url_agency_ids: list[int] = []
16+
for agency in url.confirmed_agencies:
17+
url_agency_ids.append(agency.id)
18+
19+
return DataSourceGetResponse(
20+
url_id=mapping[URL.id],
21+
url=mapping[URL.url],
22+
name=mapping[URL.name],
23+
record_type=mapping[URLRecordType.record_type],
24+
agency_ids=url_agency_ids,
25+
description=mapping[URL.description],
26+
batch_id=mapping[LinkBatchURL.batch_id],
27+
record_formats=mapping[URLOptionalDataSourceMetadata.record_formats] or [],
28+
data_portal_type=mapping[URLOptionalDataSourceMetadata.data_portal_type],
29+
supplying_entity=mapping[URLOptionalDataSourceMetadata.supplying_entity],
30+
coverage_start=mapping[URLOptionalDataSourceMetadata.coverage_start],
31+
coverage_end=mapping[URLOptionalDataSourceMetadata.coverage_end],
32+
agency_supplied=mapping[URLOptionalDataSourceMetadata.agency_supplied],
33+
agency_aggregation=mapping[URLOptionalDataSourceMetadata.agency_aggregation],
34+
agency_originated=mapping[URLOptionalDataSourceMetadata.agency_originated],
35+
agency_described_not_in_database=mapping[URLOptionalDataSourceMetadata.agency_described_not_in_database],
36+
update_method=mapping[URLOptionalDataSourceMetadata.update_method],
37+
readme_url=mapping[URLOptionalDataSourceMetadata.readme_url],
38+
originating_entity=mapping[URLOptionalDataSourceMetadata.originating_entity],
39+
retention_schedule=mapping[URLOptionalDataSourceMetadata.retention_schedule],
40+
scraper_url=mapping[URLOptionalDataSourceMetadata.scraper_url],
41+
submission_notes=mapping[URLOptionalDataSourceMetadata.submission_notes],
42+
access_notes=mapping[URLOptionalDataSourceMetadata.access_notes],
43+
access_types=mapping[URLOptionalDataSourceMetadata.access_types] or []
44+
)

src/api/endpoints/data_source/by_id/get/__init__.py

Whitespace-only changes.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from sqlalchemy import Select, RowMapping
2+
from sqlalchemy.ext.asyncio import AsyncSession
3+
4+
from src.api.endpoints.data_source._shared.build import build_data_source_get_query
5+
from src.api.endpoints.data_source._shared.process import process_data_source_get_mapping
6+
from src.api.endpoints.data_source.get.response import DataSourceGetResponse
7+
from src.db.models.impl.url.core.sqlalchemy import URL
8+
from src.db.queries.base.builder import QueryBuilderBase
9+
10+
11+
class GetDataSourceByIDQueryBuilder(QueryBuilderBase):
12+
def __init__(
13+
self,
14+
url_id: int,
15+
):
16+
super().__init__()
17+
self.url_id = url_id
18+
19+
async def run(self, session: AsyncSession) -> DataSourceGetResponse:
20+
query: Select = build_data_source_get_query()
21+
query = query.where(URL.id == self.url_id)
22+
23+
mapping: RowMapping = await self.sh.mapping(session, query=query)
24+
return process_data_source_get_mapping(mapping=mapping)

src/api/endpoints/data_source/get/query.py

Lines changed: 8 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from datetime import date
22
from typing import Any, Sequence
33

4-
from sqlalchemy import select, RowMapping, and_
4+
from sqlalchemy import select, RowMapping, and_, Select
55
from sqlalchemy.ext.asyncio import AsyncSession
66
from sqlalchemy.orm import selectinload
77

8+
from src.api.endpoints.data_source._shared.build import build_data_source_get_query
9+
from src.api.endpoints.data_source._shared.process import process_data_source_get_mapping
810
from src.api.endpoints.data_source.get.response import DataSourceGetOuterResponse, DataSourceGetResponse
911
from src.core.enums import RecordType
1012
from src.db.models.impl.flag.url_validated.enums import URLType
@@ -18,7 +20,7 @@
1820
from src.db.queries.base.builder import QueryBuilderBase
1921

2022

21-
class GetDataSourceQueryBuilder(QueryBuilderBase):
23+
class GetDataSourcesQueryBuilder(QueryBuilderBase):
2224

2325
def __init__(
2426
self,
@@ -28,59 +30,9 @@ def __init__(
2830
self.page = page
2931

3032
async def run(self, session: AsyncSession) -> DataSourceGetOuterResponse:
33+
query: Select = build_data_source_get_query()
3134
query = (
32-
select(
33-
URL,
34-
URL.id,
35-
URL.url,
36-
37-
# Required Attributes
38-
URL.name,
39-
URLRecordType.record_type,
40-
41-
# Optional Attributes
42-
URL.description,
43-
LinkBatchURL.batch_id,
44-
URLOptionalDataSourceMetadata.record_formats,
45-
URLOptionalDataSourceMetadata.data_portal_type,
46-
URLOptionalDataSourceMetadata.supplying_entity,
47-
URLOptionalDataSourceMetadata.coverage_start,
48-
URLOptionalDataSourceMetadata.coverage_end,
49-
URLOptionalDataSourceMetadata.agency_supplied,
50-
URLOptionalDataSourceMetadata.agency_aggregation,
51-
URLOptionalDataSourceMetadata.agency_described_not_in_database,
52-
URLOptionalDataSourceMetadata.agency_originated,
53-
URLOptionalDataSourceMetadata.update_method,
54-
URLOptionalDataSourceMetadata.readme_url,
55-
URLOptionalDataSourceMetadata.originating_entity,
56-
URLOptionalDataSourceMetadata.retention_schedule,
57-
URLOptionalDataSourceMetadata.scraper_url,
58-
URLOptionalDataSourceMetadata.submission_notes,
59-
URLOptionalDataSourceMetadata.access_notes,
60-
URLOptionalDataSourceMetadata.access_types
61-
)
62-
.join(
63-
URLRecordType,
64-
URLRecordType.url_id == URL.id
65-
)
66-
.join(
67-
FlagURLValidated,
68-
and_(
69-
FlagURLValidated.url_id == URL.id,
70-
FlagURLValidated.type == URLType.DATA_SOURCE
71-
)
72-
)
73-
.outerjoin(
74-
LinkBatchURL,
75-
LinkBatchURL.url_id == URL.id
76-
)
77-
.outerjoin(
78-
URLOptionalDataSourceMetadata,
79-
URLOptionalDataSourceMetadata.url_id == URL.id
80-
)
81-
.options(
82-
selectinload(URL.confirmed_agencies),
83-
)
35+
query
8436
.limit(100)
8537
.offset((self.page - 1) * 100)
8638
)
@@ -89,64 +41,8 @@ async def run(self, session: AsyncSession) -> DataSourceGetOuterResponse:
8941
responses: list[DataSourceGetResponse] = []
9042

9143
for mapping in mappings:
92-
url: URL = mapping[URL]
93-
url_id: int = mapping[URL.id]
94-
url_url: str = mapping[URL.url]
95-
url_name: str = mapping[URL.name]
96-
url_record_type: RecordType = mapping[URLRecordType.record_type]
97-
98-
url_agency_ids: list[int] = []
99-
for agency in url.confirmed_agencies:
100-
url_agency_ids.append(agency.id)
101-
102-
url_description: str | None = mapping[URL.description]
103-
link_batch_url_batch_id: int | None = mapping[LinkBatchURL.batch_id]
104-
url_record_formats: list[str] = mapping[URLOptionalDataSourceMetadata.record_formats] or []
105-
url_data_portal_type: str | None = mapping[URLOptionalDataSourceMetadata.data_portal_type]
106-
url_supplying_entity: str | None = mapping[URLOptionalDataSourceMetadata.supplying_entity]
107-
url_coverage_start: date | None = mapping[URLOptionalDataSourceMetadata.coverage_start]
108-
url_coverage_end: date | None = mapping[URLOptionalDataSourceMetadata.coverage_end]
109-
url_agency_supplied: bool | None = mapping[URLOptionalDataSourceMetadata.agency_supplied]
110-
url_agency_aggregation: AgencyAggregationEnum | None = mapping[URLOptionalDataSourceMetadata.agency_aggregation]
111-
url_agency_originated: bool | None = mapping[URLOptionalDataSourceMetadata.agency_originated]
112-
url_agency_described_not_in_database: bool | None = mapping[URLOptionalDataSourceMetadata.agency_described_not_in_database]
113-
url_update_method: UpdateMethodEnum | None = mapping[URLOptionalDataSourceMetadata.update_method]
114-
url_readme_url: str | None = mapping[URLOptionalDataSourceMetadata.readme_url]
115-
url_originating_entity: str | None = mapping[URLOptionalDataSourceMetadata.originating_entity]
116-
url_retention_schedule: RetentionScheduleEnum | None = mapping[URLOptionalDataSourceMetadata.retention_schedule]
117-
url_scraper_url: str | None = mapping[URLOptionalDataSourceMetadata.scraper_url]
118-
url_submission_notes: str | None = mapping[URLOptionalDataSourceMetadata.submission_notes]
119-
url_access_notes: str | None = mapping[URLOptionalDataSourceMetadata.access_notes]
120-
url_access_types: list[AccessTypeEnum] = mapping[URLOptionalDataSourceMetadata.access_types] or []
121-
122-
responses.append(
123-
DataSourceGetResponse(
124-
url_id=url_id,
125-
url=url_url,
126-
name=url_name,
127-
record_type=url_record_type,
128-
agency_ids=url_agency_ids,
129-
description=url_description,
130-
batch_id=link_batch_url_batch_id,
131-
record_formats=url_record_formats,
132-
data_portal_type=url_data_portal_type,
133-
supplying_entity=url_supplying_entity,
134-
coverage_start=url_coverage_start,
135-
coverage_end=url_coverage_end,
136-
agency_supplied=url_agency_supplied,
137-
agency_aggregation=url_agency_aggregation,
138-
agency_originated=url_agency_originated,
139-
agency_described_not_in_database=url_agency_described_not_in_database,
140-
update_method=url_update_method,
141-
readme_url=url_readme_url,
142-
originating_entity=url_originating_entity,
143-
retention_schedule=url_retention_schedule,
144-
scraper_url=url_scraper_url,
145-
submission_notes=url_submission_notes,
146-
access_notes=url_access_notes,
147-
access_types=url_access_types
148-
)
149-
)
44+
response: DataSourceGetResponse = process_data_source_get_mapping(mapping)
45+
responses.append(response)
15046

15147
return DataSourceGetOuterResponse(
15248
results=responses,

src/api/endpoints/data_source/routes.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
from src.api.endpoints.data_source.by_id.agency.get.wrapper import get_data_source_agencies_wrapper
77
from src.api.endpoints.data_source.by_id.agency.post.wrapper import add_data_source_agency_link
88
from src.api.endpoints.data_source.by_id.agency.shared.check import check_is_data_source_url
9-
from src.api.endpoints.data_source.get.query import GetDataSourceQueryBuilder
10-
from src.api.endpoints.data_source.get.response import DataSourceGetOuterResponse
9+
from src.api.endpoints.data_source.by_id.get.query import GetDataSourceByIDQueryBuilder
10+
from src.api.endpoints.data_source.get.query import GetDataSourcesQueryBuilder
11+
from src.api.endpoints.data_source.get.response import DataSourceGetOuterResponse, DataSourceGetResponse
1112
from src.api.endpoints.data_source.by_id.put.query import UpdateDataSourceQueryBuilder
1213
from src.api.endpoints.data_source.by_id.put.request import DataSourcePutRequest
1314
from src.api.shared.models.message_response import MessageResponse
@@ -28,7 +29,16 @@ async def get_data_sources(
2829
),
2930
) -> DataSourceGetOuterResponse:
3031
return await async_core.adb_client.run_query_builder(
31-
GetDataSourceQueryBuilder(page=page)
32+
GetDataSourcesQueryBuilder(page=page)
33+
)
34+
35+
@data_sources_router.get("/{url_id}")
36+
async def get_data_source_by_id(
37+
url_id: int,
38+
async_core: AsyncCore = Depends(get_async_core),
39+
) -> DataSourceGetResponse:
40+
return await async_core.adb_client.run_query_builder(
41+
GetDataSourceByIDQueryBuilder(url_id)
3242
)
3343

3444
@data_sources_router.put("/{url_id}")
@@ -81,3 +91,4 @@ async def remove_agency_from_data_source(
8191
adb_client=async_core.adb_client
8292
)
8393
return MessageResponse(message="Agency removed from data source.")
94+

tests/automated/integration/readonly/api/data_sources/by_id/__init__.py

Whitespace-only changes.

tests/automated/integration/readonly/api/data_sources/by_id/agencies/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)