bugdantic 0.2.6__tar.gz → 0.2.7__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.7
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,3 +1,4 @@
1
+ import base64
1
2
  import enum
2
3
  import json
3
4
  import logging
@@ -7,7 +8,7 @@ from typing import Any, Mapping, MutableMapping, Optional, Sequence
7
8
  from urllib.parse import urljoin
8
9
 
9
10
  import httpx
10
- from pydantic import BaseModel, ConfigDict
11
+ from pydantic import BaseModel, ConfigDict, field_validator
11
12
 
12
13
  Json = dict[str, "Json"] | list["Json"] | str | int | float | bool | None
13
14
 
@@ -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,34 @@ 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: Optional[str] = None
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
+ @field_validator("data", mode="before")
360
+ @classmethod
361
+ def encode_data(cls, value: bytes | str) -> str:
362
+ if isinstance(value, bytes):
363
+ return base64.b64encode(value).decode("ascii")
364
+ return value
365
+
366
+
367
+ class AttachmentCreateResponse(BaseModel):
368
+ ids: list[int]
369
+
370
+
295
371
  @dataclass
296
372
  class BugzillaConfig:
297
373
  base_url: str
@@ -343,6 +419,9 @@ class Bugzilla:
343
419
 
344
420
  if self.config.allow_writes or method in {"GET", "OPTIONS", "HEAD"}:
345
421
  retry = 0
422
+ if self.config.max_retries < 0:
423
+ raise ValueError("max_retries must be at least 0")
424
+ response = None
346
425
  while retry <= self.config.max_retries:
347
426
  retry += 1
348
427
  response = self.client.request(
@@ -350,6 +429,7 @@ class Bugzilla:
350
429
  )
351
430
  if response.status_code != 503:
352
431
  break
432
+ assert response is not None
353
433
  try:
354
434
  response.raise_for_status()
355
435
  except Exception as e:
@@ -490,3 +570,28 @@ class Bugzilla:
490
570
  return update_result.bugs
491
571
 
492
572
  return []
573
+
574
+ def create_attachment(
575
+ self, attachment: AttachmentCreate, bug_id: Optional[int] = None
576
+ ) -> list[int]:
577
+ """Add an attachment to one or more bugs"""
578
+ if bug_id is not None:
579
+ id_str = str(bug_id)
580
+ elif attachment.ids:
581
+ id_str = str(attachment.ids[0])
582
+ else:
583
+ raise ValueError("Missing bug ids for attachment")
584
+
585
+ path = f"bug/{id_str}/attachment"
586
+
587
+ json_body = attachment.model_dump(exclude_none=True)
588
+ if not json_body.get("ids"):
589
+ json_body["ids"] = [bug_id]
590
+
591
+ response = self.request("POST", path, json_body=json_body)
592
+
593
+ if self.config.allow_writes:
594
+ create_result = AttachmentCreateResponse.model_validate(response)
595
+ return create_result.ids
596
+
597
+ return []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bugdantic
3
- Version: 0.2.6
3
+ Version: 0.2.7
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.7"
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