Skip to content

Commit 468b39e

Browse files
authored
Fix Firebase Release initialization from API response data (#473)
1 parent 6d48649 commit 468b39e

File tree

10 files changed

+111
-13
lines changed

10 files changed

+111
-13
lines changed

.pre-commit-config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repos:
22
- repo: https://github.com/pre-commit/pre-commit-hooks
3-
rev: v5.0.0
3+
rev: v6.0.0
44
hooks:
55
- id: end-of-file-fixer
66
- id: trailing-whitespace
@@ -14,13 +14,13 @@ repos:
1414
- id: black
1515
language_version: python3.11
1616
- repo: https://github.com/astral-sh/ruff-pre-commit
17-
rev: v0.11.13
17+
rev: v0.12.10
1818
hooks:
1919
- id: ruff
2020
args: [ --fix ]
2121
- id: ruff-format
2222
- repo: https://github.com/pre-commit/mirrors-mypy
23-
rev: v1.16.0
23+
rev: v1.17.1
2424
hooks:
2525
- id: mypy
2626
additional_dependencies:

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Version 0.60.0
22
-------------
33

4+
**Bugfixes**
5+
- Fix action `firebase-app-distribution-debug get-latest-build-version` by supporting new `updateTime` and `expireTime` timestamps for releases. [PR #473](https://github.com/codemagic-ci-cd/cli-tools/pull/473)
6+
7+
Version 0.60.0
8+
-------------
9+
410
This release contains changes from [PR #469](https://github.com/codemagic-ci-cd/cli-tools/pull/469).
511

612
**Features**

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "codemagic-cli-tools"
3-
version = "0.60.0"
3+
version = "0.60.1"
44
description = "CLI tools used in Codemagic builds"
55
readme = "README.md"
66
authors = [

src/codemagic/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
__title__ = "codemagic-cli-tools"
22
__description__ = "CLI tools used in Codemagic builds"
3-
__version__ = "0.60.0.dev"
3+
__version__ = "0.60.1.dev"
44
__url__ = "https://github.com/codemagic-ci-cd/cli-tools"
55
__licence__ = "GNU General Public License v3.0"

src/codemagic/google/resources/firebase/release.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from datetime import datetime
55
from datetime import timezone
66
from typing import Optional
7+
from typing import overload
78

89
from codemagic.google.resources import Resource
910

@@ -24,9 +25,32 @@ class Release(Resource):
2425
testingUri: str
2526
binaryDownloadUri: str
2627
releaseNotes: Optional[ReleaseNotes] = None
28+
updateTime: Optional[datetime] = None
29+
expireTime: Optional[datetime] = None
2730

2831
def __post_init__(self):
29-
if isinstance(self.createTime, str):
30-
self.createTime = datetime.fromisoformat(self.createTime.rstrip("Z")).replace(tzinfo=timezone.utc)
32+
self.createTime = self._parse_datetime(self.createTime)
33+
self.updateTime = self._parse_datetime(self.updateTime)
34+
self.expireTime = self._parse_datetime(self.expireTime)
3135
if isinstance(self.releaseNotes, dict):
3236
self.releaseNotes = ReleaseNotes(self.releaseNotes["text"])
37+
38+
@classmethod
39+
@overload
40+
def _parse_datetime(cls, timestamp: str) -> datetime: ...
41+
42+
@classmethod
43+
@overload
44+
def _parse_datetime(cls, timestamp: datetime) -> datetime: ...
45+
46+
@classmethod
47+
@overload
48+
def _parse_datetime(cls, timestamp: None) -> None: ...
49+
50+
@classmethod
51+
def _parse_datetime(cls, timestamp: str | datetime | None) -> datetime | None:
52+
if isinstance(timestamp, datetime):
53+
return timestamp
54+
elif isinstance(timestamp, str):
55+
return datetime.fromisoformat(timestamp.rstrip("Z")).replace(tzinfo=timezone.utc)
56+
return None

src/codemagic/google/resources/resource.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,50 @@
11
from __future__ import annotations
22

3+
import dataclasses
34
import enum
45
import re
5-
from typing import Any
6-
from typing import Tuple
6+
from abc import ABCMeta
7+
from typing import TYPE_CHECKING
78

89
from codemagic.models import DictSerializable
910
from codemagic.models import JsonSerializable
11+
from codemagic.models import JsonSerializableMeta
12+
from codemagic.utilities import log
1013

14+
if TYPE_CHECKING:
15+
from typing import Any
16+
from typing import Dict
17+
from typing import Mapping
18+
from typing import Tuple
19+
from typing import Type
20+
from typing import TypeVar
21+
22+
R = TypeVar("R", bound="Resource")
23+
24+
25+
class ResourceAbcMeta(JsonSerializableMeta, ABCMeta):
26+
pass
27+
28+
29+
@dataclasses.dataclass
30+
class Resource(DictSerializable, JsonSerializable, metaclass=ResourceAbcMeta):
31+
@classmethod
32+
def _get_defined_fields(cls, given_fields: Mapping[str, Any]) -> Dict[str, Any]:
33+
logger = log.get_logger(cls, log_to_stream=False)
34+
defined_fields = {f.name for f in dataclasses.fields(cls)}
35+
fields = {}
36+
for field_name, field_value in given_fields.items():
37+
if field_name in defined_fields:
38+
fields[field_name] = field_value
39+
else:
40+
logger.warning("Unknown field %r for resource %s", field_name, cls.__name__)
41+
return fields
42+
43+
@classmethod
44+
def from_api_response(cls: Type[R], api_response: Mapping[str, Any]) -> R:
45+
defined_fields = cls._get_defined_fields(api_response)
46+
return cls(**defined_fields)
1147

12-
class Resource(DictSerializable, JsonSerializable):
1348
@staticmethod
1449
def _format_attribute_name(name: str) -> str:
1550
name = re.sub(r"([a-z])([A-Z])", r"\1 \2", name)

src/codemagic/google/services/firebase/releases_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,4 @@ def list(
6767
next_page_token = response["nextPageToken"]
6868

6969
self._logger.debug("Listed %d Firebase releases for app %r", len(firebase_releases[:limit]), app_id)
70-
return [Release(**cast(dict, firebase_release)) for firebase_release in firebase_releases[:limit]]
70+
return [Release.from_api_response(firebase_release) for firebase_release in firebase_releases[:limit]]

tests/google/resources/firebase/test_release.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ def test_release_string_representation(api_firebase_release: dict):
2222
Testing URI: https://appdistribution.firebase.google.com/testerapps/1:146661841143:android:a8d456e0c8b5e71bd11bf2/releases/78ruoe1t1uvr8
2323
Binary download URI: https://firebaseappdistribution.googleapis.com/app-binary-downloads/projects/146661841143/apps/1:146661841143:android:a8d456e0c8b5e71bd11bf2/releases/78ruoe1t1uvr8/binaries/fcdd844be2bd504ae7bb9d672731ddbfcd89c1419a9aa746a0bb6f67d3a1429d/app.apk?token=token
2424
Release notes:
25-
Text: My release notes""",
25+
Text: My release notes
26+
Update time: 2025-08-25T07:04:39.166957+00:00
27+
Expire time: 2026-01-22T07:04:23.208387+00:00
28+
""",
2629
).strip()
2730
assert str(release) == expected

tests/google/resources/mocks/firebase_release.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@
88
"binaryDownloadUri": "https://firebaseappdistribution.googleapis.com/app-binary-downloads/projects/146661841143/apps/1:146661841143:android:a8d456e0c8b5e71bd11bf2/releases/78ruoe1t1uvr8/binaries/fcdd844be2bd504ae7bb9d672731ddbfcd89c1419a9aa746a0bb6f67d3a1429d/app.apk?token=token",
99
"releaseNotes": {
1010
"text": "My release notes"
11-
}
11+
},
12+
"updateTime": "2025-08-25T07:04:39.166957+00:00",
13+
"expireTime": "2026-01-22T07:04:23.208387+00:00"
1214
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import dataclasses
2+
from unittest import mock
3+
4+
import pytest
5+
6+
from codemagic.google.resources import Resource
7+
8+
9+
@pytest.fixture
10+
def mock_logger():
11+
mock_logger = mock.MagicMock()
12+
with mock.patch("codemagic.utilities.log.get_logger", return_value=mock_logger):
13+
yield mock_logger
14+
15+
16+
def test_from_api_response(mock_logger: mock.MagicMock):
17+
@dataclasses.dataclass
18+
class MyGoogleResource(Resource):
19+
field: str
20+
21+
resource = MyGoogleResource.from_api_response(
22+
{
23+
"field": "value",
24+
"undefined": 100,
25+
},
26+
)
27+
assert resource == MyGoogleResource(field="value")
28+
mock_logger.warning.assert_called_once_with("Unknown field %r for resource %s", "undefined", "MyGoogleResource")

0 commit comments

Comments
 (0)