Skip to content

Commit 0830755

Browse files
authored
Merge pull request #55 from alan-turing-institute/update-pydantic
Update pydantic and clarify date logic
2 parents 89eed79 + 1fc64f2 commit 0830755

File tree

12 files changed

+710
-503
lines changed

12 files changed

+710
-503
lines changed

controller_function/poetry.lock

Lines changed: 251 additions & 151 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

controller_function/pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@ azure-identity = "^1.16.0"
1414
azure-mgmt-resource = "^23.1.1"
1515
azure-mgmt-subscription = "^3.1.1"
1616
opencensus-ext-azure = "^1.1.13"
17-
pydantic = "^2.7.1"
18-
pydantic-settings = "^2.2.1"
17+
pydantic = "^2.11.5"
18+
pydantic-settings = "^2.9.1"
1919
PyJWT = {extras = ["crypto"], version = "^2.8.0"}
2020
requests = "^2.31.0"
21-
rctab_models = { git = "https://github.com/alan-turing-institute/rctab-models", tag = "0.1.0" }
21+
rctab_models = { git = "https://github.com/alan-turing-institute/rctab-models", tag = "0.2.0" }
2222

2323
[tool.poetry.group.dev.dependencies]
2424
coverage = "^7.5.1"
2525
flake8 = "^3.9.2"
2626
isort = "^5.13.2"
27-
mypy = "^1.10.0"
27+
mypy = "^1.15.0"
2828
pylint = "^3.2.0"
2929
pylint-absolute-imports = "^1.1.0"
3030
types-requests = "^2.31.0.20240406" # Should match requests version

status_function/poetry.lock

Lines changed: 116 additions & 113 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

status_function/pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ azure-mgmt-authorization = "^4.0.0"
1515
azure-mgmt-subscription = "^3.1.1"
1616
msgraph-sdk = "^1.28.0"
1717
opencensus-ext-azure = "^1.1.14"
18-
pydantic = "^2.11.3"
19-
pydantic-settings = "^2.8.1"
18+
pydantic = "^2.11.5"
19+
pydantic-settings = "^2.9.1"
2020
PyJWT = {extras = ["crypto"], version = "^2.10.1"}
2121
requests = "^2.32.3"
22-
rctab_models = { git = "https://github.com/alan-turing-institute/rctab-models", tag = "0.1.0" }
22+
rctab_models = { git = "https://github.com/alan-turing-institute/rctab-models", tag = "0.2.0" }
2323

2424
[tool.poetry.group.dev.dependencies]
2525
coverage = "^7.8.0"

usage_function/monthly_usage/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,13 @@ def main(mytimer: func.TimerRequest) -> None:
9898
usage_item.monthly_upload = today
9999

100100
logger.warning("Sending usage for %s", dates)
101-
send_usage(config.API_URL, usage_items, monthly_usage_upload=True)
101+
send_usage(
102+
config.API_URL,
103+
usage_items,
104+
date_from,
105+
date_to,
106+
monthly_usage_upload=True,
107+
)
102108

103109
logger.warning("Monthly usage function finished.")
104110
return

usage_function/poetry.lock

Lines changed: 274 additions & 205 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

usage_function/pyproject.toml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,18 @@ azure-mgmt-costmanagement = "^4.0.1"
1919
azure-mgmt-managementgroups = "^1.0.0"
2020
azure-mgmt-subscription = "^3.1.1"
2121
opencensus-ext-azure = "^1.1.13"
22-
pydantic = "^2.7.1"
23-
pydantic-settings = "^2.2.1"
22+
pydantic = "^2.11.5"
23+
pydantic-settings = "^2.9.1"
2424
PyJWT = {extras = ["crypto"], version = "^2.8.0"}
2525
requests = "^2.31.0"
26-
rctab_models = { git = "https://github.com/alan-turing-institute/rctab-models", tag = "0.1.0" }
26+
rctab_models = { git = "https://github.com/alan-turing-institute/rctab-models", tag = "0.2.0" }
2727

2828
[tool.poetry.group.dev.dependencies]
2929
coverage = "^7.5.1"
30-
flake8 = "^3.9.2"
3130
isort = "^5.13.2"
32-
mypy = "^1.10.0"
31+
mypy = "^1.15.0"
3332
pre-commit = "^3.7.1"
34-
pylint = "^3.2.0"
33+
pylint = "^3.3.7"
3534
pylint-absolute-imports = "^1.1.0"
3635
types-requests = "^2.31.0.20240406" # Should match requests version
3736

usage_function/run_tests.sh

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,11 @@ status=0
99
# shellcheck disable=SC1091
1010
source .venv/bin/activate
1111

12-
# Check Python coding style with flake8, using black's default max line length
13-
# To auto-format a file, you can run `python -m black filename.py`
14-
echo "Running flake8..."
15-
python -m flake8 --max-line-length=88 --exclude=.venv,venv
16-
status=$((status+$?))
17-
18-
1912
# Run here rather than as a pre-commit hook so that local imports happen last
2013
echo "Running isort..."
2114
isort . --profile=black
2215
status=$((status+$?))
2316

24-
2517
# Find all .py files (ignoring .venv or venv directories) and check their
2618
# code style with pylint, using (something close to) Google's default config
2719
echo "Running pylint..."

usage_function/tests/test_function_app.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def test_main(self) -> None:
3535
mock_get_all_usage.return_value = ["usage1", "usage2"]
3636

3737
with patch("usage.datetime") as mock_datetime:
38-
now = datetime.now()
38+
now = datetime(year=2022, month=7, day=10)
3939
mock_datetime.now.return_value = now
4040

4141
with patch(
@@ -61,9 +61,11 @@ def test_main(self) -> None:
6161
mock_timer.past_due = True
6262

6363
usage.main(mock_timer)
64+
start_datetime = datetime(2022, 7, 8)
65+
end_datetime = datetime(2022, 7, 9)
6466

6567
mock_date_range.assert_called_once_with(
66-
now - timedelta(days=2), now - timedelta(days=1)
68+
start_datetime, end_datetime
6769
)
6870

6971
mock_get_all_usage.assert_has_calls(
@@ -87,6 +89,8 @@ def test_main(self) -> None:
8789
call(
8890
HTTP_ADAPTER.validate_python("https://my.host"),
8991
["usage1", "usage2"],
92+
start_datetime.date(),
93+
end_datetime.date(),
9094
),
9195
]
9296
)

usage_function/tests/test_utils.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,23 +135,32 @@ def test_retrieve_and_send_usage(self) -> None:
135135
)
136136

137137
with patch("utils.usage.BearerAuth") as mock_auth:
138+
# Patch POST so that it returns an error (300 status code).
138139
with patch("requests.post") as mock_post:
139140
mock_response = MagicMock()
140141
mock_response.status_code = 300
141142
mock_response.text = "some-mock-text"
142143
mock_post.return_value = mock_response
143144

145+
sept_2021 = date(2021, 9, 1)
146+
144147
with patch("utils.usage.logging.warning") as mock_log:
145148
with self.assertRaises(RuntimeError):
146149
utils.usage.retrieve_and_send_usage(
147150
HTTP_ADAPTER.validate_python("https://123.123.123.123"),
148151
[example_usage_detail], # type: ignore
152+
sept_2021,
153+
sept_2021,
149154
)
150155

151156
usage = models.Usage(**usage_dict)
152157

153158
expected_data = (
154-
models.AllUsage(usage_list=[usage])
159+
models.AllUsage(
160+
usage_list=[usage],
161+
start_date=sept_2021,
162+
end_date=sept_2021,
163+
)
155164
.model_dump_json()
156165
.encode("utf-8")
157166
)
@@ -171,6 +180,7 @@ def test_retrieve_and_send_usage(self) -> None:
171180
"some-mock-text",
172181
)
173182

183+
# Patch POST so that it returns a success (200 status code).
174184
with patch("requests.post") as mock_post:
175185
mock_response = MagicMock()
176186
mock_response.status_code = 200
@@ -180,12 +190,18 @@ def test_retrieve_and_send_usage(self) -> None:
180190
utils.usage.retrieve_and_send_usage(
181191
HTTP_ADAPTER.validate_python("https://123.123.123.123"),
182192
[example_usage_detail], # type: ignore
193+
sept_2021,
194+
sept_2021,
183195
)
184196

185197
usage = models.Usage(**usage_dict)
186198

187199
expected_data = (
188-
models.AllUsage(usage_list=[usage])
200+
models.AllUsage(
201+
usage_list=[usage],
202+
start_date=sept_2021,
203+
end_date=sept_2021,
204+
)
189205
.model_dump_json()
190206
.encode("utf-8")
191207
)

usage_function/usage/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def main(mytimer: func.TimerRequest) -> None:
3232
if mytimer.past_due:
3333
logger.info("The timer is past due.")
3434

35-
now = datetime.now()
35+
now = datetime.now().replace(hour=0, microsecond=0, second=0, minute=0)
3636
start_datetime = (
3737
now
3838
- timedelta(days=config.USAGE_HISTORY_DAYS - 1)
@@ -58,7 +58,12 @@ def main(mytimer: func.TimerRequest) -> None:
5858
)
5959

6060
try:
61-
retrieve_and_send_usage(config.API_URL, usage)
61+
retrieve_and_send_usage(
62+
config.API_URL,
63+
usage,
64+
start_datetime.date(),
65+
end_datetime.date(),
66+
)
6267
break
6368
except HttpResponseError as e:
6469
logger.error("Request to azure failed. Trying again in 60 seconds")

usage_function/utils/usage.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import copy
44
import logging
5-
from datetime import datetime, timedelta
5+
from datetime import date, datetime, timedelta
66
from functools import lru_cache
77
from typing import Generator, Iterable, Optional
88
from uuid import UUID
@@ -11,7 +11,7 @@
1111
from azure.identity import DefaultAzureCredential
1212
from azure.mgmt.consumption import ConsumptionManagementClient
1313
from azure.mgmt.consumption.models import UsageDetailsListResult
14-
from pydantic_core import Url
14+
from pydantic import HttpUrl
1515
from rctab_models import models
1616

1717
from utils.auth import BearerAuth
@@ -186,22 +186,29 @@ def retrieve_usage(
186186

187187

188188
def retrieve_and_send_usage(
189-
hostname_or_ip: Url, usage_data: Iterable[UsageDetailsListResult]
189+
hostname_or_ip: HttpUrl,
190+
usage_data: Iterable[UsageDetailsListResult],
191+
start_date: date,
192+
end_date: date,
190193
) -> None:
191194
"""Retrieve usage data from Azure and send it to the API.
192195
193196
Args:
194197
hostname_or_ip: Hostname or IP of the API.
195198
usage_data: Usage data object.
199+
start_date: The start of the date range that has been collected.
200+
end_date: The inclusive end of the date range that has been collected.
196201
"""
197202
usage_list = retrieve_usage(usage_data)
198203

199-
send_usage(hostname_or_ip, usage_list)
204+
send_usage(hostname_or_ip, usage_list, start_date, end_date)
200205

201206

202207
def send_usage(
203-
hostname_or_ip: Url,
208+
hostname_or_ip: HttpUrl,
204209
all_item_list: list[models.Usage],
210+
start_date: date,
211+
end_date: date,
205212
monthly_usage_upload: bool = False,
206213
) -> None:
207214
"""Post each item of usage_data to a route."""
@@ -221,7 +228,13 @@ def get_first_run_time() -> datetime:
221228

222229
# Note that omitting the encoding appears to work but will
223230
# fail server-side with some characters, such as en-dash.
224-
data = models.AllUsage(usage_list=all_item_list).model_dump_json().encode("utf-8")
231+
data = (
232+
models.AllUsage(
233+
usage_list=all_item_list, start_date=start_date, end_date=end_date
234+
)
235+
.model_dump_json()
236+
.encode("utf-8")
237+
)
225238

226239
for _ in range(2):
227240
resp = requests.post(

0 commit comments

Comments
 (0)