Skip to content

Commit 4730b27

Browse files
authored
feat(mongodb): assume UTC for naive expires_at in custom APIs (scaleway#1341)
1 parent 4df52ca commit 4730b27

File tree

7 files changed

+255
-0
lines changed

7 files changed

+255
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from __future__ import annotations
2+
3+
from datetime import datetime, timezone
4+
from typing import Optional, Any
5+
6+
from .api import MongodbV1API
7+
8+
9+
def _ensure_tzaware_utc(value: Optional[datetime]) -> Optional[datetime]:
10+
if value is None:
11+
return None
12+
if value.tzinfo is None or value.tzinfo.utcoffset(value) is None:
13+
return value.replace(tzinfo=timezone.utc)
14+
return value
15+
16+
17+
class MongodbUtilsV1API(MongodbV1API):
18+
"""
19+
Async extensions for MongoDB V1.
20+
- Naive datetimes for expires_at are assumed to be UTC.
21+
"""
22+
23+
async def create_snapshot(self, **kwargs: Any) -> Any:
24+
expires_at = kwargs.get("expires_at")
25+
kwargs["expires_at"] = _ensure_tzaware_utc(expires_at)
26+
return await super().create_snapshot(**kwargs)
27+
28+
async def update_snapshot(self, **kwargs: Any) -> Any:
29+
expires_at = kwargs.get("expires_at")
30+
kwargs["expires_at"] = _ensure_tzaware_utc(expires_at)
31+
return await super().update_snapshot(**kwargs)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from __future__ import annotations
2+
3+
from datetime import datetime, timezone
4+
from typing import Optional, Any
5+
6+
from .api import MongodbV1Alpha1API
7+
8+
9+
def _ensure_tzaware_utc(value: Optional[datetime]) -> Optional[datetime]:
10+
if value is None:
11+
return None
12+
if value.tzinfo is None or value.tzinfo.utcoffset(value) is None:
13+
return value.replace(tzinfo=timezone.utc)
14+
return value
15+
16+
17+
class MongodbUtilsV1Alpha1API(MongodbV1Alpha1API):
18+
"""
19+
Async extensions for MongoDB V1alpha1.
20+
- Naive datetimes for expires_at are assumed to be UTC.
21+
"""
22+
23+
async def create_snapshot(self, **kwargs: Any) -> Any:
24+
expires_at = kwargs.get("expires_at")
25+
kwargs["expires_at"] = _ensure_tzaware_utc(expires_at)
26+
return await super().create_snapshot(**kwargs)
27+
28+
async def update_snapshot(self, **kwargs: Any) -> Any:
29+
expires_at = kwargs.get("expires_at")
30+
kwargs["expires_at"] = _ensure_tzaware_utc(expires_at)
31+
return await super().update_snapshot(**kwargs)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from __future__ import annotations
2+
3+
from datetime import datetime, timezone
4+
from typing import Any, Optional
5+
6+
from scaleway.mongodb.v1.api import MongodbV1API # type: ignore[import-untyped]
7+
8+
9+
def _ensure_tzaware_utc(value: Optional[datetime]) -> Optional[datetime]:
10+
if value is None:
11+
return None
12+
if value.tzinfo is None or value.tzinfo.utcoffset(value) is None:
13+
return value.replace(tzinfo=timezone.utc)
14+
return value
15+
16+
17+
class MongodbUtilsV1API(MongodbV1API): # type: ignore[misc]
18+
"""
19+
Extensions for MongoDB V1 that provide safer ergonomics.
20+
21+
- Naive datetimes for expires_at are assumed to be UTC.
22+
"""
23+
24+
def create_snapshot(self, **kwargs: Any) -> Any:
25+
expires_at = kwargs.get("expires_at")
26+
kwargs["expires_at"] = _ensure_tzaware_utc(expires_at)
27+
return super().create_snapshot(**kwargs)
28+
29+
def update_snapshot(self, **kwargs: Any) -> Any:
30+
expires_at = kwargs.get("expires_at")
31+
kwargs["expires_at"] = _ensure_tzaware_utc(expires_at)
32+
return super().update_snapshot(**kwargs)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# MongoDB tests (VCR)
2+
3+
This suite uses VCR cassettes to replay HTTP calls in CI.
4+
5+
How to record locally:
6+
7+
1. Ensure you have valid Scaleway credentials exported (access key, secret key, default region).
8+
2. Pick a MongoDB instance to target for snapshot creation.
9+
3. Temporarily replace the fixed `instance_id` in `test_custom_api.py` with your instance id or set breakpoints to inject it.
10+
4. Run the specific test once to record the cassette:
11+
12+
```bash
13+
pytest -k test_create_snapshot_with_naive_expires_at_vcr
14+
```
15+
16+
5. Commit the generated cassette file:
17+
- Path: `scaleway/scaleway/mongodb/v1/tests/cassettes/test_create_snapshot_with_naive_expires_at_vcr.cassette.yaml`
18+
19+
Notes:
20+
- The test will skip in CI if the cassette file is missing.
21+
- After recording, restore the fixed `instance_id` value used by the test to keep requests stable across replays.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
interactions:
2+
- request:
3+
body: '{"instance_id": "dd5cd838-525b-4395-b2ae-4dec3381ceaf", "name": "sdk-python-test-snapshot",
4+
"expires_at": "2025-11-04T11:16:49.007353+00:00"}'
5+
headers:
6+
Content-Length:
7+
- '141'
8+
user-agent:
9+
- scaleway-sdk-python/2.0.0
10+
method: POST
11+
uri: https://api.scaleway.com/mongodb/v1/regions/fr-par/snapshots
12+
response:
13+
body:
14+
string: '{"id": "7d68eb22-4529-452b-af73-160cc621427f", "instance_id": "dd5cd838-525b-4395-b2ae-4dec3381ceaf",
15+
"instance_name": "mgdb-magical-curie", "name": "sdk-python-test-snapshot",
16+
"status": "creating", "created_at": "2025-11-03T10:16:49.164635Z", "updated_at":
17+
"2025-11-03T10:16:49.164635Z", "expires_at": "2025-11-04T11:16:49.007353Z",
18+
"size_bytes": 0, "node_type": "mgdb-play2-nano", "volume_type": "sbs_5k",
19+
"region": "fr-par"}'
20+
headers:
21+
content-length:
22+
- '415'
23+
date:
24+
- Mon, 03 Nov 2025 10:16:49 GMT
25+
server:
26+
- Scaleway API Gateway (fr-par-3;edge01)
27+
x-request-id:
28+
- 68289a8d-c836-47bb-8e92-9ff15be8b012
29+
status:
30+
code: 200
31+
message: OK
32+
- request:
33+
body: '{"instance_id": "dd5cd838-525b-4395-b2ae-4dec3381ceaf", "name": "sdk-python-test-snapshot",
34+
"expires_at": "2025-11-04T11:19:08.742558+00:00"}'
35+
headers:
36+
Content-Length:
37+
- '141'
38+
user-agent:
39+
- scaleway-sdk-python/2.0.0
40+
method: POST
41+
uri: https://api.scaleway.com/mongodb/v1/regions/fr-par/snapshots
42+
response:
43+
body:
44+
string: '{"id": "e604d001-e413-4c55-a27c-909a852a8343", "instance_id": "dd5cd838-525b-4395-b2ae-4dec3381ceaf",
45+
"instance_name": "mgdb-magical-curie", "name": "sdk-python-test-snapshot",
46+
"status": "creating", "created_at": "2025-11-03T10:19:08.876365Z", "updated_at":
47+
"2025-11-03T10:19:08.876365Z", "expires_at": "2025-11-04T11:19:08.742558Z",
48+
"size_bytes": 0, "node_type": "mgdb-play2-nano", "volume_type": "sbs_5k",
49+
"region": "fr-par"}'
50+
headers:
51+
content-length:
52+
- '415'
53+
date:
54+
- Mon, 03 Nov 2025 10:19:09 GMT
55+
server:
56+
- Scaleway API Gateway (fr-par-3;edge01)
57+
x-request-id:
58+
- 408c3493-f594-4fc6-a6eb-f00782ea820a
59+
status:
60+
code: 200
61+
message: OK
62+
version: 1
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import os
2+
from datetime import datetime, timedelta, timezone
3+
from pathlib import Path
4+
5+
import pytest
6+
7+
from vcr_config import scw_vcr
8+
from vcr_config import PYTHON_UPDATE_CASSETTE
9+
from tests.utils import initialize_client_test
10+
from scaleway.mongodb.v1.custom_api import MongodbUtilsV1API
11+
12+
13+
# mypy: ignore-errors
14+
15+
16+
@scw_vcr.use_cassette
17+
def test_create_snapshot_with_naive_expires_at_vcr() -> None:
18+
cassette = (
19+
Path(__file__).with_name("cassettes")
20+
/ "test_create_snapshot_with_naive_expires_at_vcr.cassette.yaml"
21+
)
22+
if not cassette.exists() and not os.getenv("PYTHON_UPDATE_CASSETTE"):
23+
pytest.skip(
24+
"cassette not recorded yet; set PYTHON_UPDATE_CASSETTE=true to record"
25+
)
26+
client = initialize_client_test()
27+
api = MongodbUtilsV1API(client, bypass_validation=True)
28+
29+
# During recording, require a real instance_id via env; during replay, use the fixed value matching the cassette
30+
if PYTHON_UPDATE_CASSETTE:
31+
instance_id = os.getenv("SCW_TEST_MONGODB_INSTANCE_ID")
32+
if not instance_id:
33+
pytest.skip("SCW_TEST_MONGODB_INSTANCE_ID not set while recording")
34+
else:
35+
instance_id = "00000000-0000-0000-0000-000000000000"
36+
37+
# Naive datetime should be handled as UTC by the utils API
38+
naive_dt = datetime.now(timezone.utc).replace(tzinfo=None) + timedelta(days=1)
39+
40+
snapshot = api.create_snapshot(
41+
instance_id=instance_id,
42+
name="sdk-python-test-snapshot",
43+
expires_at=naive_dt,
44+
)
45+
46+
assert snapshot is not None
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from __future__ import annotations
2+
3+
from datetime import datetime, timezone
4+
from typing import Any, Optional
5+
6+
from scaleway.mongodb.v1alpha1.api import MongodbV1Alpha1API # type: ignore[import-untyped]
7+
8+
9+
def _ensure_tzaware_utc(value: Optional[datetime]) -> Optional[datetime]:
10+
if value is None:
11+
return None
12+
if value.tzinfo is None or value.tzinfo.utcoffset(value) is None:
13+
return value.replace(tzinfo=timezone.utc)
14+
return value
15+
16+
17+
class MongodbUtilsV1Alpha1API(MongodbV1Alpha1API): # type: ignore[misc]
18+
"""
19+
Extensions for MongoDB V1alpha1 that provide safer ergonomics.
20+
21+
- Naive datetimes for expires_at are assumed to be UTC.
22+
"""
23+
24+
def create_snapshot(self, **kwargs: Any) -> Any:
25+
expires_at = kwargs.get("expires_at")
26+
kwargs["expires_at"] = _ensure_tzaware_utc(expires_at)
27+
return super().create_snapshot(**kwargs)
28+
29+
def update_snapshot(self, **kwargs: Any) -> Any:
30+
expires_at = kwargs.get("expires_at")
31+
kwargs["expires_at"] = _ensure_tzaware_utc(expires_at)
32+
return super().update_snapshot(**kwargs)

0 commit comments

Comments
 (0)