Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e98c20d

Browse files
authoredAug 8, 2024··
Merge pull request #39 from cmu-delphi/ds/ruff
refactor: switch from invoke to Makefile and use ruff for formatting and linting
2 parents 112e178 + de47b20 commit e98c20d

File tree

13 files changed

+122
-278
lines changed

13 files changed

+122
-278
lines changed
 

‎.github/workflows/ci.yml

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,16 @@ jobs:
1818
cache: "pip"
1919
- name: Install Dependencies
2020
run: |
21-
python -m venv venv
22-
source venv/bin/activate
23-
pip install -e ".[dev]"
21+
make install
2422
- name: Check Formatting
2523
run: |
26-
source venv/bin/activate
27-
inv lint-black
24+
make lint_ruff
2825
- name: Check Linting
2926
run: |
30-
source venv/bin/activate
31-
inv lint-pylint
27+
make lint_pylint
3228
- name: Check Types
3329
run: |
34-
source venv/bin/activate
35-
inv lint-mypy
30+
make lint_mypy
3631
- name: Test
3732
run: |
38-
source venv/bin/activate
39-
inv test
33+
make test

‎.github/workflows/release_helper.yml

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,13 @@ jobs:
5252
python-version: 3.8
5353
- name: Install build dependencies
5454
run: |
55-
python -m pip install --upgrade pip
56-
pip install -e ".[dev]"
55+
make install
5756
- name: Linting
5857
run: |
59-
. venv/bin/activate
60-
inv lint
58+
make lint
6159
- name: Testing
6260
run: |
63-
. venv/bin/activate
64-
inv test
61+
make test
6562
6663
build:
6764
needs: [create_release, lint]
@@ -75,11 +72,10 @@ jobs:
7572
python-version: 3.8
7673
- name: Install build dependencies
7774
run: |
78-
python -m pip install --upgrade pip
79-
pip install -e ".[dev]"
75+
make install
8076
- name: Build
8177
run: |
82-
inv dist
78+
make dist
8379
8480
release_package:
8581
needs: [create_release, lint]

‎.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ __pycache__
88
dist/
99
build/
1010
docs/_build
11-
venv/
11+
env/

‎.pre-commit-config.yaml

Lines changed: 0 additions & 10 deletions
This file was deleted.

‎Makefile

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
.PHONY = venv, lint, test, clean, release
2+
3+
venv:
4+
python3.8 -m venv env
5+
6+
install: venv
7+
env/bin/python -m pip install --upgrade pip
8+
env/bin/pip install -e ".[dev]"
9+
10+
lint_ruff:
11+
env/bin/ruff check epidatpy tests
12+
13+
lint_mypy:
14+
env/bin/mypy epidatpy tests
15+
16+
lint_pylint:
17+
env/bin/pylint epidatpy tests
18+
19+
lint: lint_ruff lint_mypy lint_pylint
20+
21+
format:
22+
env/bin/ruff format epidatpy tests
23+
24+
test:
25+
env/bin/pytest .
26+
27+
docs:
28+
env/bin/sphinx-build -b html docs docs/_build
29+
python -m webbrowser -t "docs/_build/index.html"
30+
31+
clean_docs:
32+
rm -rf docs/_build
33+
34+
clean_build:
35+
rm -rf build dist .eggs
36+
find . -name '*.egg-info' -exec rm -rf {} +
37+
find . -name '*.egg' -exec rm -f {} +
38+
39+
clean_python:
40+
find . -name '*.pyc' -exec rm -f {} +
41+
find . -name '*.pyo' -exec rm -f {} +
42+
find . -name '__pycache__' -exec rm -fr {} +
43+
44+
clean: clean_docs clean_build clean_python
45+
46+
release: clean lint test
47+
env/bin/python -m build --sdist --wheel
48+
49+
upload: release
50+
env/bin/twine upload dist/*

‎README.md

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,17 @@ TODO
2222

2323
## Development
2424

25-
Prepare virtual environment and install dependencies
25+
The following commands are available for developers:
2626

2727
```sh
28-
python -m venv venv
29-
source ./venv/bin/activate
30-
pip install -e ".[dev]"
31-
```
32-
33-
### Common Commands
34-
35-
```sh
36-
source ./venv/bin/activate
37-
inv format # format code
38-
inv lint # check linting
39-
inv docs # build docs
40-
inv test # run unit tests
41-
inv coverage # run unit tests with coverage
42-
inv clean # clean build artifacts
43-
inv dist # build distribution packages
44-
inv release # upload the current version to pypi
28+
make install # setup venv, install dependencies and local package
29+
make test # run unit tests
30+
make format # format code
31+
make lint # check linting
32+
make docs # build docs
33+
make dist # build distribution packages
34+
make release # upload the current version to pypi
35+
make clean # clean build and docs artifacts
4536
```
4637

4738
### Release Process

‎epidatpy/_covidcast.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@
3333

3434
@dataclass
3535
class WebLink:
36-
"""
37-
represents a web link
36+
"""represents a web link
3837
"""
3938

4039
alt: str
@@ -43,8 +42,7 @@ class WebLink:
4342

4443
@dataclass
4544
class DataSignalGeoStatistics:
46-
"""
47-
COVIDcast signal statistics
45+
"""COVIDcast signal statistics
4846
"""
4947

5048
min: float
@@ -64,7 +62,11 @@ def define_covidcast_fields() -> List[EpidataFieldInfo]:
6462
EpidataFieldInfo("signal", EpidataFieldType.text),
6563
EpidataFieldInfo("geo_type", EpidataFieldType.categorical, categories=list(get_args(GeoType))),
6664
EpidataFieldInfo("geo_value", EpidataFieldType.text),
67-
EpidataFieldInfo("time_type", EpidataFieldType.categorical, categories=list(get_args(TimeType))),
65+
EpidataFieldInfo(
66+
"time_type",
67+
EpidataFieldType.categorical,
68+
categories=list(get_args(TimeType)),
69+
),
6870
EpidataFieldInfo("time_value", EpidataFieldType.date_or_epiweek),
6971
EpidataFieldInfo("issue", EpidataFieldType.date),
7072
EpidataFieldInfo("lag", EpidataFieldType.int),
@@ -80,8 +82,7 @@ def define_covidcast_fields() -> List[EpidataFieldInfo]:
8082

8183
@dataclass
8284
class DataSignal(Generic[CALL_TYPE]):
83-
"""
84-
represents a COVIDcast data signal
85+
"""represents a COVIDcast data signal
8586
"""
8687

8788
_create_call: Callable[[Mapping[str, Optional[EpiRangeParam]]], CALL_TYPE]
@@ -160,7 +161,7 @@ def call(
160161
lag: Optional[int] = None,
161162
) -> CALL_TYPE:
162163
"""Fetch Delphi's COVID-19 Surveillance Streams"""
163-
if any((v is None for v in (geo_type, geo_values, time_values))):
164+
if any(v is None for v in (geo_type, geo_values, time_values)):
164165
raise InvalidArgumentException("`geo_type`, `time_values`, and `geo_values` are all required")
165166
if issues is not None and lag is not None:
166167
raise InvalidArgumentException("`issues` and `lag` are mutually exclusive")
@@ -194,8 +195,7 @@ def __call__(
194195

195196
@dataclass
196197
class DataSource(Generic[CALL_TYPE]):
197-
"""
198-
represents a COVIDcast data source
198+
"""represents a COVIDcast data source
199199
"""
200200

201201
_create_call: InitVar[Callable[[Mapping[str, Optional[EpiRangeParam]]], CALL_TYPE]]
@@ -247,8 +247,7 @@ def signal_df(self) -> DataFrame:
247247

248248
@dataclass
249249
class CovidcastDataSources(Generic[CALL_TYPE]):
250-
"""
251-
COVIDcast data source helper.
250+
"""COVIDcast data source helper.
252251
"""
253252

254253
sources: Sequence[DataSource[CALL_TYPE]]

‎epidatpy/_endpoints.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ def get_wildcard_equivalent_dates(time_value: EpiRangeParam, time_type: Literal[
3636

3737

3838
class AEpiDataEndpoints(ABC, Generic[CALL_TYPE]):
39-
"""
40-
epidata endpoint list and fetcher
39+
"""epidata endpoint list and fetcher
4140
"""
4241

4342
@abstractmethod
@@ -87,8 +86,7 @@ def pub_covid_hosp_facility_lookup(
8786
fips_code: Optional[str] = None,
8887
) -> CALL_TYPE:
8988
"""Lookup COVID hospitalization facility identifiers."""
90-
91-
if all((v is None for v in (state, ccn, city, zip, fips_code))):
89+
if all(v is None for v in (state, ccn, city, zip, fips_code)):
9290
raise InvalidArgumentException("one of `state`, `ccn`, `city`, `zip`, or `fips_code` is required")
9391

9492
return self._create_call(
@@ -121,7 +119,6 @@ def pub_covid_hosp_facility(
121119
publication_dates: Optional[EpiRangeParam] = None,
122120
) -> CALL_TYPE:
123121
"""Fetch COVID hospitalization data for specific facilities."""
124-
125122
collection_weeks = get_wildcard_equivalent_dates(collection_weeks, "day")
126123

127124
# Confusingly, the endpoint expects `collection_weeks` to be in day format,
@@ -271,7 +268,6 @@ def pub_covid_hosp_state_timeseries(
271268
as_of: Union[None, int, str] = None,
272269
) -> CALL_TYPE:
273270
"""Fetch COVID hospitalization data."""
274-
275271
if issues is not None and as_of is not None:
276272
raise InvalidArgumentException("`issues` and `as_of` are mutually exclusive")
277273

@@ -481,7 +477,6 @@ def pub_covidcast(
481477

482478
def pub_delphi(self, system: str, epiweek: Union[int, str]) -> CALL_TYPE:
483479
"""Fetch Delphi's forecast."""
484-
485480
return self._create_call(
486481
"delphi/",
487482
{"system": system, "epiweek": epiweek},

‎epidatpy/_model.py

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,17 @@
3535
StringParam = Union[str, Sequence[str]]
3636
IntParam = Union[int, Sequence[int]]
3737
ParamType = Union[StringParam, IntParam, EpiRangeParam]
38-
EpiDataResponse = TypedDict("EpiDataResponse", {"result": int, "message": str, "epidata": List})
3938
CALL_TYPE = TypeVar("CALL_TYPE")
4039

4140

41+
class EpiDataResponse(TypedDict):
42+
"""response from the API"""
43+
44+
result: int
45+
message: str
46+
epidata: List
47+
48+
4249
def format_date(d: EpiDateLike) -> str:
4350
if isinstance(d, date):
4451
# YYYYMMDD
@@ -70,9 +77,7 @@ def format_list(values: EpiRangeParam) -> str:
7077

7178

7279
class EpiRange:
73-
"""
74-
Range object for dates/epiweeks
75-
"""
80+
"""Range object for dates/epiweeks"""
7681

7782
def __init__(self, start: EpiDateLike, end: EpiDateLike) -> None:
7883
# check if types are correct
@@ -91,21 +96,15 @@ def __str__(self) -> str:
9196

9297

9398
class InvalidArgumentException(Exception):
94-
"""
95-
exception for an invalid argument
96-
"""
99+
"""exception for an invalid argument"""
97100

98101

99102
class OnlySupportsClassicFormatException(Exception):
100-
"""
101-
the endpoint only supports the classic message format, due to an non-standard behavior
102-
"""
103+
"""the endpoint only supports the classic message format, due to an non-standard behavior"""
103104

104105

105106
class EpidataFieldType(Enum):
106-
"""
107-
field type
108-
"""
107+
"""field type"""
109108

110109
text = 0
111110
int = 1
@@ -119,9 +118,7 @@ class EpidataFieldType(Enum):
119118

120119
@dataclass
121120
class EpidataFieldInfo:
122-
"""
123-
meta data information about an return field
124-
"""
121+
"""meta data information about an return field"""
125122

126123
name: Final[str] = ""
127124
type: Final[EpidataFieldType] = EpidataFieldType.text
@@ -137,9 +134,7 @@ def add_endpoint_to_url(url: str, endpoint: str) -> str:
137134

138135

139136
class AEpiDataCall:
140-
"""
141-
base epidata call class
142-
"""
137+
"""base epidata call class"""
143138

144139
_base_url: Final[str]
145140
_endpoint: Final[str]
@@ -167,16 +162,19 @@ def __init__(
167162
self.meta_by_name = {k.name: k for k in self.meta}
168163
# Set the use_cache value from the constructor if present.
169164
# Otherwise check the USE_EPIDATPY_CACHE variable, accepting various "truthy" values.
170-
self.use_cache = use_cache if use_cache is not None \
171-
else (environ.get("USE_EPIDATPY_CACHE", "").lower() in ['true', 't', '1'])
165+
self.use_cache = (
166+
use_cache
167+
if use_cache is not None
168+
else (environ.get("USE_EPIDATPY_CACHE", "").lower() in ["true", "t", "1"])
169+
)
172170
# Set cache_max_age_days from the constructor, fall back to environment variable.
173171
if cache_max_age_days:
174172
self.cache_max_age_days = cache_max_age_days
175173
else:
176174
env_days = environ.get("EPIDATPY_CACHE_MAX_AGE_DAYS", "7")
177175
if env_days.isdigit():
178176
self.cache_max_age_days = int(env_days)
179-
else: # handle string / negative / invalid enviromment variable
177+
else: # handle string / negative / invalid enviromment variable
180178
self.cache_max_age_days = 7
181179

182180
def _verify_parameters(self) -> None:
@@ -187,9 +185,7 @@ def _formatted_parameters(
187185
self,
188186
fields: Optional[Sequence[str]] = None,
189187
) -> Mapping[str, str]:
190-
"""
191-
format this call into a [URL, Params] tuple
192-
"""
188+
"""Format this call into a [URL, Params] tuple"""
193189
all_params = dict(self._params)
194190
if fields:
195191
all_params["fields"] = fields
@@ -199,9 +195,7 @@ def request_arguments(
199195
self,
200196
fields: Optional[Sequence[str]] = None,
201197
) -> Tuple[str, Mapping[str, str]]:
202-
"""
203-
format this call into a [URL, Params] tuple
204-
"""
198+
"""Format this call into a [URL, Params] tuple"""
205199
formatted_params = self._formatted_parameters(fields)
206200
full_url = add_endpoint_to_url(self._base_url, self._endpoint)
207201
return full_url, formatted_params
@@ -210,9 +204,7 @@ def request_url(
210204
self,
211205
fields: Optional[Sequence[str]] = None,
212206
) -> str:
213-
"""
214-
format this call into a full HTTP request url with encoded parameters
215-
"""
207+
"""Format this call into a full HTTP request url with encoded parameters"""
216208
self._verify_parameters()
217209
u, p = self.request_arguments(fields)
218210
query = urlencode(p)

‎epidatpy/_parse.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ def parse_user_date_or_week(
7171
raise ValueError(f"Cannot parse date or week from {value}")
7272

7373

74-
def fields_to_predicate(fields: Optional[Sequence[str]] = None) -> Callable[[str], bool]:
74+
def fields_to_predicate(
75+
fields: Optional[Sequence[str]] = None,
76+
) -> Callable[[str], bool]:
7577
if not fields:
7678
return lambda _: True
7779
to_include: Set[str] = set()

‎pyproject.toml

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ classifiers = [
3030
]
3131
requires-python = ">=3.8"
3232
dependencies = [
33-
"aiohttp",
3433
"appdirs",
3534
"diskcache",
3635
"epiweeks>=2.1",
@@ -41,34 +40,31 @@ dependencies = [
4140

4241
[project.optional-dependencies]
4342
dev = [
44-
"black",
45-
"coverage",
46-
"invoke",
4743
"mypy",
48-
"pre-commit",
4944
"pylint",
5045
"pytest",
5146
"recommonmark",
47+
"ruff",
5248
"sphinx_rtd_theme",
5349
"sphinx-autodoc-typehints",
5450
"sphinx",
5551
"twine",
5652
"types-requests",
57-
"watchdog",
58-
"wheel",
5953
]
6054

6155
[project.urls]
6256
homepage = "https://github.com/cmu-delphi/epidatpy"
6357
repository = "https://github.com/cmu-delphi/epidatpy"
6458

6559

66-
[tool.black]
67-
line-length = 120
68-
target-version = ['py38']
69-
7060
[tool.ruff]
71-
lint.extend-select = ["I"]
61+
line-length = 120
62+
format.docstring-code-format = true
63+
format.docstring-code-line-length = "dynamic"
64+
lint.extend-select = [
65+
"I", # isort
66+
"UP", # pyupgrade
67+
]
7268

7369
[tool.pylint]
7470
max-line-length = 120

‎tasks.py

Lines changed: 0 additions & 161 deletions
This file was deleted.

‎tests/test_epidata_calls.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -320,15 +320,15 @@ def test_pub_nidss_flu(self) -> None:
320320
assert str(data["ili"].dtype) == "Float64"
321321

322322
@pytest.mark.skipif(not secret_norostat, reason="Norostat key not available.")
323+
@pytest.mark.skip(reason="TODO: Need working Norostat query.")
323324
def test_pvt_norostat(self) -> None:
324325
apicall = EpiDataContext().pvt_norostat(auth=secret_norostat, location="1", epiweeks=201233)
325326
data = apicall.df()
326327

327-
# TODO: Need a non-trivial query for Norostat
328-
# assert len(data) > 0
329-
# assert str(data["release_date"].dtype) == "datetime64[ns]"
330-
# assert str(data["epiweek"].dtype) == "string"
331-
# assert str(data["value"].dtype) == "Int64"
328+
assert len(data) > 0
329+
assert str(data["release_date"].dtype) == "datetime64[ns]"
330+
assert str(data["epiweek"].dtype) == "string"
331+
assert str(data["value"].dtype) == "Int64"
332332

333333
def test_pub_nowcast(self) -> None:
334334
apicall = EpiDataContext().pub_nowcast(locations="ca", epiweeks=EpiRange(201201, 201301))

0 commit comments

Comments
 (0)
Please sign in to comment.