Skip to content
This repository was archived by the owner on Aug 30, 2021. It is now read-only.

Commit 3306f7c

Browse files
Move around schema into logic groupings (#2)
* Move around schema into logic groups * chore: deprecation warning on import of schema * ci: isort runs only on vaccine_feed_ingest_schema * ci: explicitly configure isort config * Fix superlinter isort Co-authored-by: Eli Block <[email protected]>
1 parent ac56e92 commit 3306f7c

10 files changed

+354
-206
lines changed

.github/workflows/test.yml

+1
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,4 @@ jobs:
5959
VALIDATE_ALL_CODEBASE: false
6060
DEFAULT_BRANCH: main
6161
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
62+
PYTHON_ISORT_CONFIG_FILE: .isort.cfg # This is the default, but was not being picked up

.isort.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
[settings]
22
profile = black
3+
src_paths = vaccine_feed_ingest_schema

README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ Import this package then use it to construct normalized objects with type
2020
enforcement.
2121

2222
```python
23-
from vaccine_feed_ingest_schema import schema
23+
from vaccine_feed_ingest_schema import location
2424

2525

26-
schema.NormalizedLocation(
26+
location.NormalizedLocation(
2727
id="vaccinebot:uuid-for-site",
28-
source=schema.Source(
28+
source=location.Source(
2929
source="vaccinebot",
3030
id="uuid-for-site",
3131
fetched_from_uri="https://vaccinateTheStates.com",

tests/test_vaccine_feed_ingest_schema.py

+17-2
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
import inspect
2+
from importlib import reload
23

34
import pydantic.error_wrappers
45
import pytest
56

6-
from vaccine_feed_ingest_schema import schema
7+
DEPRECATION_SNIPPET = "vaccine_feed_ingest_schema.schema is deprecated."
78

89

10+
def test_warn_on_import():
11+
with pytest.warns(DeprecationWarning, match=DEPRECATION_SNIPPET):
12+
from vaccine_feed_ingest_schema import schema
13+
14+
# Depending on the order in which tests run, the above import may be
15+
# skipped. Reload it so that we trigger the warning, if it exists.
16+
reload(schema)
17+
18+
19+
@pytest.mark.filterwarnings(f"ignore: {DEPRECATION_SNIPPET}")
920
def test_has_expected_classes():
21+
from vaccine_feed_ingest_schema import schema
22+
1023
class_tuples = inspect.getmembers(schema, inspect.isclass)
1124
classes = list(map(lambda class_tuple: class_tuple[0], class_tuples))
1225

1326
expected_classes = [
14-
"BaseModel",
1527
"Address",
1628
"LatLng",
1729
"Contact",
@@ -37,6 +49,9 @@ def test_has_expected_classes():
3749
raise KeyError(f"Extra class {klass} defined. Did you update your tests?")
3850

3951

52+
@pytest.mark.filterwarnings(f"ignore: {DEPRECATION_SNIPPET}")
4053
def test_raises_on_invalid_location():
54+
from vaccine_feed_ingest_schema import schema
55+
4156
with pytest.raises(pydantic.error_wrappers.ValidationError):
4257
schema.NormalizedLocation()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import inspect
2+
3+
from vaccine_feed_ingest_schema import load
4+
5+
6+
def test_has_expected_classes():
7+
class_tuples = inspect.getmembers(load, inspect.isclass)
8+
classes = list(map(lambda class_tuple: class_tuple[0], class_tuples))
9+
10+
expected_classes = [
11+
"BaseModel",
12+
"NormalizedLocation",
13+
"ImportMatchAction",
14+
"ImportSourceLocation",
15+
]
16+
17+
for expected_class in expected_classes:
18+
if expected_class not in classes:
19+
raise KeyError(f"Expected class {expected_class} is not defined.")
20+
21+
for klass in classes:
22+
if klass not in expected_classes:
23+
raise KeyError(f"Extra class {klass} defined. Did you update your tests?")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import inspect
2+
3+
import pydantic.error_wrappers
4+
import pytest
5+
from vaccine_feed_ingest_schema import location
6+
7+
8+
def test_has_expected_classes():
9+
class_tuples = inspect.getmembers(location, inspect.isclass)
10+
classes = list(map(lambda class_tuple: class_tuple[0], class_tuples))
11+
12+
expected_classes = [
13+
"BaseModel",
14+
"Address",
15+
"LatLng",
16+
"Contact",
17+
"OpenDate",
18+
"OpenHour",
19+
"Availability",
20+
"Vaccine",
21+
"Access",
22+
"Organization",
23+
"Link",
24+
"Source",
25+
"NormalizedLocation",
26+
]
27+
28+
for expected_class in expected_classes:
29+
if expected_class not in classes:
30+
raise KeyError(f"Expected class {expected_class} is not defined.")
31+
32+
for klass in classes:
33+
if klass not in expected_classes:
34+
raise KeyError(f"Extra class {klass} defined. Did you update your tests?")
35+
36+
37+
def test_raises_on_invalid_location():
38+
with pytest.raises(pydantic.error_wrappers.ValidationError):
39+
location.NormalizedLocation()

vaccine_feed_ingest_schema/common.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""Common config for all schemas"""
2+
3+
import pydantic
4+
5+
6+
class BaseModel(pydantic.BaseModel):
7+
"""BaseModel for all schema to inherit from."""
8+
9+
class Config:
10+
# Strip whitespace from str values by default.
11+
# This will help have consistant data if parsers forgot to strip.
12+
anystr_strip_whitespace = True
13+
14+
# Fail if an attrbute that doesn't exist is added.
15+
# This will help reduce typos.
16+
extra = "forbid"
17+
18+
# Validate when setting attributes.
19+
# This will help trigger errors right where they occur.
20+
validate_assignment = True
21+
22+
# Store enums as string values.
23+
# This helps so people can use either strings or enums.
24+
use_enum_values = True

vaccine_feed_ingest_schema/load.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from typing import Optional
2+
3+
from .common import BaseModel
4+
from .location import NormalizedLocation
5+
6+
7+
class ImportMatchAction(BaseModel):
8+
"""Match action to take when importing a source location"""
9+
10+
id: Optional[str]
11+
action: str
12+
13+
14+
class ImportSourceLocation(BaseModel):
15+
"""Import source location record"""
16+
17+
source_uid: str
18+
source_name: str
19+
name: Optional[str]
20+
latitude: Optional[float]
21+
longitude: Optional[float]
22+
import_json: NormalizedLocation
23+
match: Optional[ImportMatchAction]
+186
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
"""Normalized Location Schema
2+
3+
Spec defined here:
4+
https://github.com/CAVaccineInventory/vaccine-feed-ingest/wiki/Normalized-Location-Schema
5+
"""
6+
7+
from typing import List, Optional
8+
9+
from .common import BaseModel
10+
11+
12+
class Address(BaseModel):
13+
"""
14+
{
15+
"street1": str,
16+
"street2": str,
17+
"city": str,
18+
"state": str as state initial e.g. CA,
19+
"zip": str,
20+
},
21+
"""
22+
23+
street1: str
24+
street2: Optional[str]
25+
city: str
26+
state: str
27+
zip: str
28+
29+
30+
class LatLng(BaseModel):
31+
"""
32+
{
33+
"latitude": float,
34+
"longitude": float,
35+
},
36+
"""
37+
38+
latitude: float
39+
longitude: float
40+
41+
42+
class Contact(BaseModel):
43+
"""
44+
{
45+
"contact_type": str as contact type enum e.g. booking,
46+
"phone": str as (###) ###-###,
47+
"website": str,
48+
"email": str,
49+
"other": str,
50+
}
51+
"""
52+
53+
contact_type: Optional[str]
54+
phone: Optional[str]
55+
website: Optional[str]
56+
email: Optional[str]
57+
other: Optional[str]
58+
59+
60+
class OpenDate(BaseModel):
61+
"""
62+
{
63+
"opens": str as iso8601 date,
64+
"closes": str as iso8601 date,
65+
}
66+
"""
67+
68+
opens: Optional[str]
69+
closes: Optional[str]
70+
71+
72+
class OpenHour(BaseModel):
73+
"""
74+
{
75+
"day": str as day of week enum e.g. monday,
76+
"opens": str as hh:mm,
77+
"closes": str as hh:mm,
78+
}
79+
"""
80+
81+
day: str
82+
open: str
83+
closes: str
84+
85+
86+
class Availability(BaseModel):
87+
"""
88+
{
89+
"drop_in": bool,
90+
"appointments": bool,
91+
},
92+
"""
93+
94+
drop_in: Optional[bool]
95+
appointments: Optional[bool]
96+
97+
98+
class Vaccine(BaseModel):
99+
"""
100+
{
101+
"vaccine": str as vaccine type enum,
102+
"supply_level": str as supply level enum e.g. more_than_48hrs
103+
}
104+
"""
105+
106+
vaccine: str
107+
supply_level: Optional[str]
108+
109+
110+
class Access(BaseModel):
111+
"""
112+
{
113+
"walk": bool,
114+
"drive": bool,
115+
"wheelchair": str,
116+
}
117+
"""
118+
119+
walk: Optional[bool]
120+
drive: Optional[bool]
121+
wheelchair: Optional[str]
122+
123+
124+
class Organization(BaseModel):
125+
"""
126+
{
127+
"id": str as parent organization enum e.g. rite_aid,
128+
"name": str,
129+
}
130+
"""
131+
132+
id: Optional[str]
133+
name: Optional[str]
134+
135+
136+
class Link(BaseModel):
137+
"""
138+
{
139+
"authority": str as authority enum e.g. rite_aid or google_places,
140+
"id": str as id used by authority to reference this location e.g. 4096,
141+
"uri": str as uri used by authority to reference this location,
142+
}
143+
"""
144+
145+
authority: Optional[str]
146+
id: Optional[str]
147+
uri: Optional[str]
148+
149+
150+
class Source(BaseModel):
151+
"""
152+
{
153+
"source": str as source type enum e.g. vaccinespotter,
154+
"id": str as source defined id e.g. 7382088,
155+
"fetched_from_uri": str as uri where data was fetched from,
156+
"fetched_at": str as iso8601 datetime (when scraper ran),
157+
"published_at": str as iso8601 datetime (when source claims it updated),
158+
"data": {...parsed source data in source schema...},
159+
}
160+
"""
161+
162+
source: str
163+
id: str
164+
fetched_from_uri: Optional[str]
165+
fetched_at: Optional[str]
166+
published_at: Optional[str]
167+
data: dict
168+
169+
170+
class NormalizedLocation(BaseModel):
171+
id: str
172+
name: Optional[str]
173+
address: Optional[Address]
174+
location: Optional[LatLng]
175+
contact: Optional[List[Contact]]
176+
languages: Optional[List[str]] # [str as ISO 639-1 code]
177+
opening_dates: Optional[List[OpenDate]]
178+
opening_hours: Optional[List[OpenHour]]
179+
availability: Optional[Availability]
180+
inventory: Optional[List[Vaccine]]
181+
access: Optional[Access]
182+
parent_organization: Optional[Organization]
183+
links: Optional[List[Link]]
184+
notes: Optional[List[str]]
185+
active: Optional[bool]
186+
source: Source

0 commit comments

Comments
 (0)