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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bugdantic
3
- Version: 0.5.2
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.6; extra == "test"
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.78; extra == "test"
18
+ Requires-Dist: ty==0.0.80; extra == "test"
19
19
  Dynamic: license-file
@@ -0,0 +1,6 @@
1
+ from .bugzilla import ( # noqa: F401
2
+ Bugzilla,
3
+ BugzillaConfig,
4
+ BugzillaError,
5
+ BugzillaResponseError,
6
+ )
@@ -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
- pass
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 BugSearch(BaseModel):
222
+ class FaultsModel(BaseModel):
192
223
  faults: Optional[list[Any]] = None
193
- bugs: Optional[list[Bug]] = None
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(BaseModel):
209
- faults: Optional[list[Any]] = None
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: Optional[list[BugUpdateResponse]] = None
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(BaseModel, Generic[BugType]):
475
- bugs: Optional[list[BugType]] = None
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 Exception:
567
- msg = "Request failed"
568
- json_resp = response.json()
569
- if json_resp:
570
- msg += f"\n{json_resp.get('message')}"
571
- logging.error(msg)
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
- err = ErrorResponse.model_validate(data)
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
- if search_result.faults:
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
- if query_result.faults:
744
- raise BugzillaError(query_result.faults)
784
+ query_result.raise_for_faults()
745
785
  bugs = query_result.bugs
746
- if bugs is None:
747
- raise BugzillaError("Empty bugs list but no faults")
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
- if search_result.faults:
782
- raise BugzillaError(search_result.faults)
783
- if search_result.bugs is not None:
784
- results.extend(search_result.bugs)
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
- raise BugzillaError("Response contained neither bugs nor faults fields")
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.5.2
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.6; extra == "test"
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.78; extra == "test"
18
+ Requires-Dist: ty==0.0.80; extra == "test"
19
19
  Dynamic: license-file
@@ -3,7 +3,7 @@ pydantic>=2.11.3
3
3
 
4
4
  [test]
5
5
  mypy==2.3.1
6
- ruff==0.16.6
6
+ ruff==0.16.7
7
7
  pytest==9.1.1
8
8
  pytest-ruff==0.5
9
- ty==0.0.78
9
+ ty==0.0.80
@@ -5,7 +5,7 @@ authors = [
5
5
  {name = "James Graham", email = "james@hoppipolla.co.uk"}
6
6
  ]
7
7
 
8
- version = "0.5.2"
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.6",
24
+ "ruff==0.16.7",
25
25
  "pytest==9.1.1",
26
26
  "pytest-ruff==0.5",
27
- "ty==0.0.78",
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