bugdantic 0.2.6__tar.gz → 0.2.8__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.2.6
3
+ Version: 0.2.8
4
4
  Summary: Bugzilla client based on pydantic
5
5
  Author-email: James Graham <james@hoppipolla.co.uk>
6
6
  Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
@@ -11,8 +11,8 @@ License-File: LICENSE
11
11
  Requires-Dist: httpx[http2]>=0.28.1
12
12
  Requires-Dist: pydantic>=2.11.3
13
13
  Provides-Extra: test
14
- Requires-Dist: mypy==1.16.0; extra == "test"
15
- Requires-Dist: ruff==0.12.0; extra == "test"
16
- Requires-Dist: pytest==8.4.0; extra == "test"
17
- Requires-Dist: pytest-ruff==0.4.1; extra == "test"
14
+ Requires-Dist: mypy==1.18.2; extra == "test"
15
+ Requires-Dist: ruff==0.14.1; extra == "test"
16
+ Requires-Dist: pytest==8.4.2; extra == "test"
17
+ Requires-Dist: pytest-ruff==0.5; extra == "test"
18
18
  Dynamic: license-file
@@ -1,9 +1,10 @@
1
+ import base64
1
2
  import enum
2
3
  import json
3
4
  import logging
4
5
  from dataclasses import dataclass
5
6
  from datetime import datetime
6
- from typing import Any, Mapping, MutableMapping, Optional, Sequence
7
+ from typing import Any, Mapping, MutableMapping, Optional, Sequence, Self
7
8
  from urllib.parse import urljoin
8
9
 
9
10
  import httpx
@@ -55,7 +56,7 @@ class Flag(BaseModel):
55
56
  modification_date: datetime
56
57
  status: str
57
58
  setter: str
58
- requestee: str
59
+ requestee: Optional[str] = None
59
60
 
60
61
 
61
62
  class Change(BaseModel):
@@ -71,6 +72,50 @@ class History(BaseModel):
71
72
  changes: list[Change]
72
73
 
73
74
 
75
+ class BugComment(BaseModel):
76
+ """A comment retrieved from a bug (cf. :class:`Comment`, used in updates)."""
77
+
78
+ id: Optional[int] = None
79
+ bug_id: Optional[int] = None
80
+ attachment_id: Optional[int] = None
81
+ count: Optional[int] = None
82
+ text: Optional[str] = None
83
+ raw_text: Optional[str] = None
84
+ creator: Optional[str] = None
85
+ time: Optional[datetime] = None
86
+ creation_time: Optional[datetime] = None
87
+ is_private: Optional[bool] = None
88
+ is_markdown: Optional[bool] = None
89
+ tags: Optional[list[str]] = None
90
+
91
+ model_config = ConfigDict(extra="allow")
92
+
93
+ def to_dict(self) -> dict[str, Any]:
94
+ return self.model_dump(exclude_unset=True)
95
+
96
+
97
+ class Attachment(BaseModel):
98
+ id: Optional[int] = None
99
+ bug_id: Optional[int] = None
100
+ file_name: Optional[str] = None
101
+ summary: Optional[str] = None
102
+ content_type: Optional[str] = None
103
+ creator: Optional[str] = None
104
+ creation_time: Optional[datetime] = None
105
+ last_change_time: Optional[datetime] = None
106
+ size: Optional[int] = None
107
+ data: Optional[str] = None
108
+ is_private: Optional[bool] = None
109
+ is_obsolete: Optional[bool] = None
110
+ is_patch: Optional[bool] = None
111
+ flags: Optional[list[Flag]] = None
112
+
113
+ model_config = ConfigDict(extra="allow")
114
+
115
+ def to_dict(self) -> dict[str, Any]:
116
+ return self.model_dump(exclude_unset=True)
117
+
118
+
74
119
  class Bug(BaseModel):
75
120
  actual_time: Optional[float] = None
76
121
  alias: Optional[str] = None
@@ -123,6 +168,9 @@ class Bug(BaseModel):
123
168
  cf_webcompat_priority: Optional[str] = None
124
169
  cf_webcompat_score: Optional[str] = None
125
170
 
171
+ comments: Optional[list[BugComment]] = None
172
+ attachments: Optional[list[Attachment]] = None
173
+
126
174
  model_config = ConfigDict(extra="allow")
127
175
 
128
176
  def to_dict(self) -> dict[str, Any]:
@@ -292,6 +340,78 @@ class BugsUpdateResponse(BaseModel):
292
340
  faults: Optional[list[Any]] = None
293
341
 
294
342
 
343
+ # Data models for creating attachments
344
+
345
+
346
+ class AttachmentCreate(BaseModel):
347
+ ids: Optional[list[int | str]] = None
348
+ data: str
349
+ file_name: str
350
+ summary: str
351
+ content_type: str
352
+ comment: Optional[str] = None
353
+ is_markdown: Optional[bool] = None
354
+ is_patch: Optional[bool] = None
355
+ is_private: Optional[bool] = None
356
+ flags: Optional[list[FlagChange]] = None
357
+ bug_flags: Optional[list[FlagChange]] = None
358
+
359
+ @classmethod
360
+ def from_raw_data(
361
+ cls,
362
+ data: str | bytes,
363
+ file_name: str,
364
+ summary: str,
365
+ content_type: str,
366
+ ids: Optional[list[int | str]] = None,
367
+ comment: Optional[str] = None,
368
+ is_markdown: Optional[bool] = None,
369
+ is_patch: Optional[bool] = None,
370
+ is_private: Optional[bool] = None,
371
+ flags: Optional[list[FlagChange]] = None,
372
+ bug_flags: Optional[list[FlagChange]] = None,
373
+ ) -> Self:
374
+ if isinstance(data, str):
375
+ data = data.encode("utf8")
376
+ return cls(
377
+ ids=ids,
378
+ data=base64.b64encode(data).decode("ascii"),
379
+ file_name=file_name,
380
+ summary=summary,
381
+ content_type=content_type,
382
+ comment=comment,
383
+ is_markdown=is_markdown,
384
+ is_patch=is_patch,
385
+ is_private=is_private,
386
+ flags=flags,
387
+ bug_flags=bug_flags,
388
+ )
389
+
390
+
391
+ class AttachmentCreateResponse(BaseModel):
392
+ attacher: str
393
+ bug_id: int
394
+ content_type: str
395
+ creation_time: datetime
396
+ creator: str
397
+ creator_detail: User
398
+ data: str
399
+ description: str
400
+ file_name: str
401
+ flags: Optional[list[FlagChange]]
402
+ id: int
403
+ is_obsolete: bool
404
+ is_patch: bool
405
+ is_private: bool
406
+ last_change_time: datetime
407
+ size: int
408
+ summary: str
409
+
410
+
411
+ class AttachmentsCreateResponse(BaseModel):
412
+ attachments: Mapping[str, AttachmentCreateResponse]
413
+
414
+
295
415
  @dataclass
296
416
  class BugzillaConfig:
297
417
  base_url: str
@@ -343,6 +463,9 @@ class Bugzilla:
343
463
 
344
464
  if self.config.allow_writes or method in {"GET", "OPTIONS", "HEAD"}:
345
465
  retry = 0
466
+ if self.config.max_retries < 0:
467
+ raise ValueError("max_retries must be at least 0")
468
+ response = None
346
469
  while retry <= self.config.max_retries:
347
470
  retry += 1
348
471
  response = self.client.request(
@@ -350,6 +473,7 @@ class Bugzilla:
350
473
  )
351
474
  if response.status_code != 503:
352
475
  break
476
+ assert response is not None
353
477
  try:
354
478
  response.raise_for_status()
355
479
  except Exception as e:
@@ -490,3 +614,28 @@ class Bugzilla:
490
614
  return update_result.bugs
491
615
 
492
616
  return []
617
+
618
+ def create_attachment(
619
+ self, attachment: AttachmentCreate, bug_id: Optional[int] = None
620
+ ) -> list[AttachmentCreateResponse]:
621
+ """Add an attachment to one or more bugs"""
622
+ if bug_id is not None:
623
+ id_str = str(bug_id)
624
+ elif attachment.ids:
625
+ id_str = str(attachment.ids[0])
626
+ else:
627
+ raise ValueError("Missing bug ids for attachment")
628
+
629
+ path = f"bug/{id_str}/attachment"
630
+
631
+ json_body = attachment.model_dump(exclude_none=True)
632
+ if not json_body.get("ids"):
633
+ json_body["ids"] = [bug_id]
634
+
635
+ response = self.request("POST", path, json_body=json_body)
636
+
637
+ if self.config.allow_writes:
638
+ create_result = AttachmentsCreateResponse.model_validate(response)
639
+ return list(create_result.attachments.values())
640
+
641
+ return []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bugdantic
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: Bugzilla client based on pydantic
5
5
  Author-email: James Graham <james@hoppipolla.co.uk>
6
6
  Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
@@ -11,8 +11,8 @@ License-File: LICENSE
11
11
  Requires-Dist: httpx[http2]>=0.28.1
12
12
  Requires-Dist: pydantic>=2.11.3
13
13
  Provides-Extra: test
14
- Requires-Dist: mypy==1.16.0; extra == "test"
15
- Requires-Dist: ruff==0.12.0; extra == "test"
16
- Requires-Dist: pytest==8.4.0; extra == "test"
17
- Requires-Dist: pytest-ruff==0.4.1; extra == "test"
14
+ Requires-Dist: mypy==1.18.2; extra == "test"
15
+ Requires-Dist: ruff==0.14.1; extra == "test"
16
+ Requires-Dist: pytest==8.4.2; extra == "test"
17
+ Requires-Dist: pytest-ruff==0.5; extra == "test"
18
18
  Dynamic: license-file
@@ -0,0 +1,8 @@
1
+ httpx[http2]>=0.28.1
2
+ pydantic>=2.11.3
3
+
4
+ [test]
5
+ mypy==1.18.2
6
+ ruff==0.14.1
7
+ pytest==8.4.2
8
+ pytest-ruff==0.5
@@ -5,7 +5,7 @@ authors = [
5
5
  {name = "James Graham", email = "james@hoppipolla.co.uk"}
6
6
  ]
7
7
 
8
- version = "0.2.6"
8
+ version = "0.2.8"
9
9
  requires-python = ">=3.11"
10
10
  classifiers = [
11
11
  "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
@@ -20,8 +20,8 @@ dependencies = [
20
20
 
21
21
  [project.optional-dependencies]
22
22
  test = [
23
- "mypy==1.16.0",
24
- "ruff==0.12.0",
25
- "pytest==8.4.0",
26
- "pytest-ruff==0.4.1",
23
+ "mypy==1.18.2",
24
+ "ruff==0.14.1",
25
+ "pytest==8.4.2",
26
+ "pytest-ruff==0.5",
27
27
  ]
@@ -32,3 +32,14 @@ def test_serach_include_history(bugzilla):
32
32
  for expected_id, bug in zip(bugs, sorted(result, key=lambda x: x.id)):
33
33
  assert bug.id == expected_id
34
34
  assert isinstance(bug.history, list)
35
+
36
+
37
+ def test_search_include_comments_and_attachments(bugzilla):
38
+ bugs = [423488, 1749533]
39
+ result = bugzilla.search(
40
+ {"id": bugs}, include_fields=["id", "comments", "attachments"]
41
+ )
42
+ for expected_id, bug in zip(bugs, sorted(result, key=lambda x: x.id)):
43
+ assert bug.id == expected_id
44
+ assert isinstance(bug.comments, list)
45
+ assert isinstance(bug.attachments, list)
@@ -1,8 +0,0 @@
1
- httpx[http2]>=0.28.1
2
- pydantic>=2.11.3
3
-
4
- [test]
5
- mypy==1.16.0
6
- ruff==0.12.0
7
- pytest==8.4.0
8
- pytest-ruff==0.4.1
File without changes
File without changes
File without changes
File without changes