bugdantic 0.5.2__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bugdantic-0.5.2 → bugdantic-0.6.0}/PKG-INFO +3 -3
- bugdantic-0.6.0/bugdantic/__init__.py +6 -0
- {bugdantic-0.5.2 → bugdantic-0.6.0}/bugdantic/bugzilla.py +77 -44
- {bugdantic-0.5.2 → bugdantic-0.6.0}/bugdantic.egg-info/PKG-INFO +3 -3
- {bugdantic-0.5.2 → bugdantic-0.6.0}/bugdantic.egg-info/requires.txt +2 -2
- {bugdantic-0.5.2 → bugdantic-0.6.0}/pyproject.toml +3 -3
- {bugdantic-0.5.2 → bugdantic-0.6.0}/test/test_bugzilla.py +15 -1
- bugdantic-0.5.2/bugdantic/__init__.py +0 -1
- {bugdantic-0.5.2 → bugdantic-0.6.0}/LICENSE +0 -0
- {bugdantic-0.5.2 → bugdantic-0.6.0}/README.md +0 -0
- {bugdantic-0.5.2 → bugdantic-0.6.0}/bugdantic/py.typed +0 -0
- {bugdantic-0.5.2 → bugdantic-0.6.0}/bugdantic/userstory.py +0 -0
- {bugdantic-0.5.2 → bugdantic-0.6.0}/bugdantic.egg-info/SOURCES.txt +0 -0
- {bugdantic-0.5.2 → bugdantic-0.6.0}/bugdantic.egg-info/dependency_links.txt +0 -0
- {bugdantic-0.5.2 → bugdantic-0.6.0}/bugdantic.egg-info/top_level.txt +0 -0
- {bugdantic-0.5.2 → bugdantic-0.6.0}/setup.cfg +0 -0
- {bugdantic-0.5.2 → bugdantic-0.6.0}/test/test_userstory.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bugdantic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Bugzilla client based on pydantic
|
|
5
5
|
Author-email: James Graham <james@hoppipolla.co.uk>
|
|
6
6
|
License-Expression: MPL-2.0
|
|
@@ -12,8 +12,8 @@ Requires-Dist: httpx[http2]>=0.28.1
|
|
|
12
12
|
Requires-Dist: pydantic>=2.11.3
|
|
13
13
|
Provides-Extra: test
|
|
14
14
|
Requires-Dist: mypy==2.3.1; extra == "test"
|
|
15
|
-
Requires-Dist: ruff==0.16.
|
|
15
|
+
Requires-Dist: ruff==0.16.7; extra == "test"
|
|
16
16
|
Requires-Dist: pytest==9.1.1; extra == "test"
|
|
17
17
|
Requires-Dist: pytest-ruff==0.5; extra == "test"
|
|
18
|
-
Requires-Dist: ty==0.0.
|
|
18
|
+
Requires-Dist: ty==0.0.80; extra == "test"
|
|
19
19
|
Dynamic: license-file
|
|
@@ -26,8 +26,39 @@ QueryValue = Optional[str | int | float | bool]
|
|
|
26
26
|
QueryParams = Mapping[str, QueryValue | Sequence[QueryValue]]
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
class BugzillaError(Exception):
|
|
30
|
-
|
|
29
|
+
class BugzillaError(Exception): ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ResponseError(BugzillaError):
|
|
33
|
+
response_data: BaseModel
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
message: str,
|
|
38
|
+
response_data: BaseModel,
|
|
39
|
+
*args: Any,
|
|
40
|
+
**kwargs: Any,
|
|
41
|
+
):
|
|
42
|
+
super().__init__(message, *args, **kwargs)
|
|
43
|
+
self.response_data = response_data
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class BugzillaResponseError(ResponseError):
|
|
47
|
+
response_data: "ErrorResponse"
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
message: str,
|
|
52
|
+
response_data: "ErrorResponse",
|
|
53
|
+
*args: Any,
|
|
54
|
+
**kwargs: Any,
|
|
55
|
+
):
|
|
56
|
+
super().__init__(message, response_data, *args, **kwargs)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def code(self) -> int:
|
|
60
|
+
assert isinstance(self.response_data, ErrorResponse)
|
|
61
|
+
return self.response_data.code
|
|
31
62
|
|
|
32
63
|
|
|
33
64
|
class UserGroup(BaseModel):
|
|
@@ -188,9 +219,17 @@ class Bug(BaseModel):
|
|
|
188
219
|
return self.model_dump(exclude_unset=True)
|
|
189
220
|
|
|
190
221
|
|
|
191
|
-
class
|
|
222
|
+
class FaultsModel(BaseModel):
|
|
192
223
|
faults: Optional[list[Any]] = None
|
|
193
|
-
|
|
224
|
+
|
|
225
|
+
def raise_for_faults(self) -> None:
|
|
226
|
+
if self.faults:
|
|
227
|
+
# In the current BMO code this should actually be impossible
|
|
228
|
+
raise ResponseError("Unexpected faults in response", response_data=self)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class BugSearch(FaultsModel):
|
|
232
|
+
bugs: list[Bug] = []
|
|
194
233
|
|
|
195
234
|
|
|
196
235
|
# Data model for bug history
|
|
@@ -205,9 +244,8 @@ class BugHistory(BaseModel):
|
|
|
205
244
|
return self.model_dump(exclude_unset=True)
|
|
206
245
|
|
|
207
246
|
|
|
208
|
-
class BugsHistory(
|
|
209
|
-
|
|
210
|
-
bugs: Optional[list[BugHistory]] = None
|
|
247
|
+
class BugsHistory(FaultsModel):
|
|
248
|
+
bugs: list[BugHistory] = []
|
|
211
249
|
|
|
212
250
|
|
|
213
251
|
# Data models for update requests
|
|
@@ -347,8 +385,7 @@ class BugUpdateResponse(BaseModel):
|
|
|
347
385
|
|
|
348
386
|
|
|
349
387
|
class BugsUpdateResponse(BaseModel):
|
|
350
|
-
bugs:
|
|
351
|
-
faults: Optional[list[Any]] = None
|
|
388
|
+
bugs: list[BugUpdateResponse] = []
|
|
352
389
|
|
|
353
390
|
|
|
354
391
|
# Data models for creating attachments
|
|
@@ -456,6 +493,10 @@ class ErrorResponse(BaseModel):
|
|
|
456
493
|
error: bool
|
|
457
494
|
documentation: str
|
|
458
495
|
|
|
496
|
+
def raise_for_error(self) -> None:
|
|
497
|
+
if self.error:
|
|
498
|
+
raise BugzillaResponseError(self.message, response_data=self)
|
|
499
|
+
|
|
459
500
|
|
|
460
501
|
@dataclass
|
|
461
502
|
class BugzillaConfig:
|
|
@@ -471,9 +512,8 @@ class BugzillaConfig:
|
|
|
471
512
|
BugType = TypeVar("BugType", bound=BaseModel)
|
|
472
513
|
|
|
473
514
|
|
|
474
|
-
class BugSearchGeneric(
|
|
475
|
-
bugs:
|
|
476
|
-
faults: Optional[list[Any]] = None
|
|
515
|
+
class BugSearchGeneric(FaultsModel, Generic[BugType]):
|
|
516
|
+
bugs: list[BugType] = []
|
|
477
517
|
|
|
478
518
|
|
|
479
519
|
def bug_search_model(bug_type: type[BugType]) -> type[BugSearchGeneric[BugType]]:
|
|
@@ -563,12 +603,16 @@ class Bugzilla:
|
|
|
563
603
|
assert response is not None
|
|
564
604
|
try:
|
|
565
605
|
response.raise_for_status()
|
|
566
|
-
except
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
606
|
+
except httpx.HTTPStatusError as e:
|
|
607
|
+
try:
|
|
608
|
+
resp_json = response.json()
|
|
609
|
+
except ValueError:
|
|
610
|
+
resp_json = None
|
|
611
|
+
if resp_json:
|
|
612
|
+
try:
|
|
613
|
+
self.check_error(resp_json)
|
|
614
|
+
except BugzillaError as bug_error:
|
|
615
|
+
raise bug_error from e
|
|
572
616
|
raise
|
|
573
617
|
return response.json()
|
|
574
618
|
else:
|
|
@@ -580,9 +624,7 @@ class Bugzilla:
|
|
|
580
624
|
|
|
581
625
|
def check_error(self, data: Mapping[str, Json]) -> Mapping[str, Json]:
|
|
582
626
|
try:
|
|
583
|
-
|
|
584
|
-
if err.error:
|
|
585
|
-
raise BugzillaError(err.message)
|
|
627
|
+
ErrorResponse.model_validate(data).raise_for_error()
|
|
586
628
|
except pydantic.ValidationError:
|
|
587
629
|
pass
|
|
588
630
|
return data
|
|
@@ -605,8 +647,7 @@ class Bugzilla:
|
|
|
605
647
|
)
|
|
606
648
|
)
|
|
607
649
|
search_result = bug_search_model(bug_type).model_validate(data)
|
|
608
|
-
|
|
609
|
-
raise BugzillaError(search_result.faults)
|
|
650
|
+
search_result.raise_for_faults()
|
|
610
651
|
bugs = search_result.bugs
|
|
611
652
|
if not bugs:
|
|
612
653
|
return None
|
|
@@ -740,11 +781,12 @@ class Bugzilla:
|
|
|
740
781
|
self.request("GET", f"bug/{bug_id}/history", params=params)
|
|
741
782
|
)
|
|
742
783
|
query_result = BugsHistory.model_validate(data)
|
|
743
|
-
|
|
744
|
-
raise BugzillaError(query_result.faults)
|
|
784
|
+
query_result.raise_for_faults()
|
|
745
785
|
bugs = query_result.bugs
|
|
746
|
-
if bugs
|
|
747
|
-
raise
|
|
786
|
+
if not bugs:
|
|
787
|
+
raise ResponseError(
|
|
788
|
+
"Empty bugs list but no error", response_data=query_result
|
|
789
|
+
)
|
|
748
790
|
assert len(bugs) == 1
|
|
749
791
|
return bugs[0]
|
|
750
792
|
|
|
@@ -778,17 +820,15 @@ class Bugzilla:
|
|
|
778
820
|
)
|
|
779
821
|
)
|
|
780
822
|
search_result = bug_search_model(bug_type).model_validate(response)
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
if not paginate or len(search_result.bugs) < page_size:
|
|
786
|
-
break
|
|
787
|
-
else:
|
|
788
|
-
logging.error(
|
|
789
|
-
f"Invalid bugzilla response object: {json.dumps(response)}"
|
|
823
|
+
search_result.raise_for_faults()
|
|
824
|
+
if not search_result.bugs:
|
|
825
|
+
raise ResponseError(
|
|
826
|
+
"Empty bugs list but no error", response_data=search_result
|
|
790
827
|
)
|
|
791
|
-
|
|
828
|
+
|
|
829
|
+
results.extend(search_result.bugs)
|
|
830
|
+
if not paginate or len(search_result.bugs) < page_size:
|
|
831
|
+
break
|
|
792
832
|
|
|
793
833
|
offset += page_size
|
|
794
834
|
query["offset"] = str(offset)
|
|
@@ -837,13 +877,6 @@ class Bugzilla:
|
|
|
837
877
|
|
|
838
878
|
if self.config.allow_writes:
|
|
839
879
|
update_result = BugsUpdateResponse.model_validate(response)
|
|
840
|
-
if update_result.faults:
|
|
841
|
-
raise BugzillaError(update_result.faults)
|
|
842
|
-
if update_result.bugs is None:
|
|
843
|
-
logging.error(
|
|
844
|
-
f"Invalid bugzilla response object: {json.dumps(response)}"
|
|
845
|
-
)
|
|
846
|
-
raise BugzillaError("Response contained neither bugs nor faults fields")
|
|
847
880
|
return update_result.bugs
|
|
848
881
|
|
|
849
882
|
return []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bugdantic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Bugzilla client based on pydantic
|
|
5
5
|
Author-email: James Graham <james@hoppipolla.co.uk>
|
|
6
6
|
License-Expression: MPL-2.0
|
|
@@ -12,8 +12,8 @@ Requires-Dist: httpx[http2]>=0.28.1
|
|
|
12
12
|
Requires-Dist: pydantic>=2.11.3
|
|
13
13
|
Provides-Extra: test
|
|
14
14
|
Requires-Dist: mypy==2.3.1; extra == "test"
|
|
15
|
-
Requires-Dist: ruff==0.16.
|
|
15
|
+
Requires-Dist: ruff==0.16.7; extra == "test"
|
|
16
16
|
Requires-Dist: pytest==9.1.1; extra == "test"
|
|
17
17
|
Requires-Dist: pytest-ruff==0.5; extra == "test"
|
|
18
|
-
Requires-Dist: ty==0.0.
|
|
18
|
+
Requires-Dist: ty==0.0.80; extra == "test"
|
|
19
19
|
Dynamic: license-file
|
|
@@ -5,7 +5,7 @@ authors = [
|
|
|
5
5
|
{name = "James Graham", email = "james@hoppipolla.co.uk"}
|
|
6
6
|
]
|
|
7
7
|
|
|
8
|
-
version = "0.
|
|
8
|
+
version = "0.6.0"
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
license = "MPL-2.0"
|
|
11
11
|
classifiers = [
|
|
@@ -21,10 +21,10 @@ dependencies = [
|
|
|
21
21
|
[project.optional-dependencies]
|
|
22
22
|
test = [
|
|
23
23
|
"mypy==2.3.1",
|
|
24
|
-
"ruff==0.16.
|
|
24
|
+
"ruff==0.16.7",
|
|
25
25
|
"pytest==9.1.1",
|
|
26
26
|
"pytest-ruff==0.5",
|
|
27
|
-
"ty==0.0.
|
|
27
|
+
"ty==0.0.80",
|
|
28
28
|
]
|
|
29
29
|
|
|
30
30
|
[tool.mypy]
|
|
@@ -3,7 +3,7 @@ from typing import Any
|
|
|
3
3
|
import pytest
|
|
4
4
|
from pydantic import BaseModel, Field
|
|
5
5
|
|
|
6
|
-
from bugdantic import Bugzilla, BugzillaConfig
|
|
6
|
+
from bugdantic import Bugzilla, BugzillaConfig, BugzillaResponseError
|
|
7
7
|
from bugdantic.bugzilla import BugComment
|
|
8
8
|
|
|
9
9
|
|
|
@@ -142,3 +142,17 @@ def test_comment_as(bugzilla):
|
|
|
142
142
|
assert all(isinstance(item, CommentReactions) for item in result)
|
|
143
143
|
assert {item.id for item in result} == {18109498, 18109500}
|
|
144
144
|
assert all(isinstance(item.reactions, dict) for item in result)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_error_code(bugzilla):
|
|
148
|
+
with pytest.raises(BugzillaResponseError) as exc_info:
|
|
149
|
+
bugzilla.comments([0])
|
|
150
|
+
assert exc_info.value.response_data.code == 111
|
|
151
|
+
assert str(exc_info.value) == "0 is not a valid comment id."
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_error_fails_whole_request(bugzilla):
|
|
155
|
+
"""One bad comment id loses the readable comments in the same request"""
|
|
156
|
+
with pytest.raises(BugzillaResponseError) as exc_info:
|
|
157
|
+
bugzilla.comments([18109498, 0, 18109500])
|
|
158
|
+
assert exc_info.value.response_data.code == 111
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .bugzilla import Bugzilla, BugzillaConfig # noqa: F401
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|