bugdantic 0.3.0__tar.gz → 0.4.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.3.0 → bugdantic-0.4.0}/PKG-INFO +1 -1
- {bugdantic-0.3.0 → bugdantic-0.4.0}/bugdantic/bugzilla.py +105 -14
- {bugdantic-0.3.0 → bugdantic-0.4.0}/bugdantic.egg-info/PKG-INFO +1 -1
- {bugdantic-0.3.0 → bugdantic-0.4.0}/pyproject.toml +1 -1
- {bugdantic-0.3.0 → bugdantic-0.4.0}/test/test_bugzilla.py +27 -1
- {bugdantic-0.3.0 → bugdantic-0.4.0}/LICENSE +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.0}/README.md +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.0}/bugdantic/__init__.py +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.0}/bugdantic/py.typed +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.0}/bugdantic/userstory.py +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.0}/bugdantic.egg-info/SOURCES.txt +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.0}/bugdantic.egg-info/dependency_links.txt +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.0}/bugdantic.egg-info/requires.txt +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.0}/bugdantic.egg-info/top_level.txt +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.0}/setup.cfg +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.0}/test/test_userstory.py +0 -0
|
@@ -4,10 +4,21 @@ import json
|
|
|
4
4
|
import logging
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from datetime import datetime
|
|
7
|
-
from typing import
|
|
7
|
+
from typing import (
|
|
8
|
+
Any,
|
|
9
|
+
Generic,
|
|
10
|
+
Mapping,
|
|
11
|
+
MutableMapping,
|
|
12
|
+
Optional,
|
|
13
|
+
Sequence,
|
|
14
|
+
Self,
|
|
15
|
+
TypeVar,
|
|
16
|
+
cast,
|
|
17
|
+
)
|
|
8
18
|
from urllib.parse import urljoin
|
|
9
19
|
|
|
10
20
|
import httpx
|
|
21
|
+
import pydantic
|
|
11
22
|
from pydantic import BaseModel, ConfigDict
|
|
12
23
|
|
|
13
24
|
Json = dict[str, "Json"] | list["Json"] | str | int | float | bool | None
|
|
@@ -412,6 +423,13 @@ class AttachmentsCreateResponse(BaseModel):
|
|
|
412
423
|
attachments: Mapping[str, AttachmentCreateResponse]
|
|
413
424
|
|
|
414
425
|
|
|
426
|
+
class ErrorResponse(BaseModel):
|
|
427
|
+
code: int
|
|
428
|
+
message: str
|
|
429
|
+
error: bool
|
|
430
|
+
documentation: str
|
|
431
|
+
|
|
432
|
+
|
|
415
433
|
@dataclass
|
|
416
434
|
class BugzillaConfig:
|
|
417
435
|
base_url: str
|
|
@@ -422,6 +440,20 @@ class BugzillaConfig:
|
|
|
422
440
|
max_retries: int = 1
|
|
423
441
|
|
|
424
442
|
|
|
443
|
+
BugType = TypeVar("BugType", bound=BaseModel)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class BugSearchGeneric(BaseModel, Generic[BugType]):
|
|
447
|
+
bugs: Optional[list[BugType]] = None
|
|
448
|
+
faults: Optional[list[Any]] = None
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def bug_search_model(bug_type: type[BugType]) -> type[BugSearchGeneric[BugType]]:
|
|
452
|
+
return cast(
|
|
453
|
+
type[BugSearchGeneric[BugType]], BugSearchGeneric.__class_getitem__(bug_type)
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
|
|
425
457
|
class Bugzilla:
|
|
426
458
|
def __init__(self, config: BugzillaConfig):
|
|
427
459
|
self.config = config
|
|
@@ -491,13 +523,27 @@ class Bugzilla:
|
|
|
491
523
|
""")
|
|
492
524
|
return {}
|
|
493
525
|
|
|
494
|
-
def
|
|
495
|
-
|
|
496
|
-
|
|
526
|
+
def check_error(self, data: Mapping[str, Json]) -> Mapping[str, Json]:
|
|
527
|
+
try:
|
|
528
|
+
err = ErrorResponse.model_validate(data)
|
|
529
|
+
if err.error:
|
|
530
|
+
raise BugzillaError(err.message)
|
|
531
|
+
except pydantic.ValidationError:
|
|
532
|
+
pass
|
|
533
|
+
return data
|
|
534
|
+
|
|
535
|
+
def _bug(
|
|
536
|
+
self,
|
|
537
|
+
bug_id: int,
|
|
538
|
+
bug_type: type[BugType],
|
|
539
|
+
include_fields: Optional[list[str]] = None,
|
|
540
|
+
) -> Optional[BugType]:
|
|
497
541
|
"""Get a single bug specified by id"""
|
|
498
|
-
|
|
542
|
+
|
|
543
|
+
data = self.check_error(
|
|
499
544
|
self.request("GET", f"bug/{bug_id}", include_fields=include_fields)
|
|
500
545
|
)
|
|
546
|
+
search_result = bug_search_model(bug_type).model_validate(data)
|
|
501
547
|
if search_result.faults:
|
|
502
548
|
raise BugzillaError(search_result.faults)
|
|
503
549
|
bugs = search_result.bugs
|
|
@@ -506,6 +552,15 @@ class Bugzilla:
|
|
|
506
552
|
assert len(bugs) == 1
|
|
507
553
|
return bugs[0]
|
|
508
554
|
|
|
555
|
+
def bug(
|
|
556
|
+
self, bug_id: int, include_fields: Optional[list[str]] = None
|
|
557
|
+
) -> Optional[Bug]:
|
|
558
|
+
return self._bug(bug_id, Bug, include_fields)
|
|
559
|
+
|
|
560
|
+
def bug_as(self, bug_id: int, bug_type: type[BugType]) -> Optional[BugType]:
|
|
561
|
+
include_fields = list(bug_type.model_fields.keys())
|
|
562
|
+
return self._bug(bug_id, bug_type, include_fields)
|
|
563
|
+
|
|
509
564
|
def bugs(
|
|
510
565
|
self,
|
|
511
566
|
bug_ids: Sequence[int],
|
|
@@ -525,6 +580,25 @@ class Bugzilla:
|
|
|
525
580
|
)
|
|
526
581
|
return results
|
|
527
582
|
|
|
583
|
+
def bugs_as(
|
|
584
|
+
self,
|
|
585
|
+
bug_ids: Sequence[int],
|
|
586
|
+
bug_type: type[BugType],
|
|
587
|
+
page_size: int = 100,
|
|
588
|
+
) -> list[Bug]:
|
|
589
|
+
"""Get multiple bugs specified by id"""
|
|
590
|
+
results: list[Bug] = []
|
|
591
|
+
for bug_ids_chunk in [
|
|
592
|
+
bug_ids[n : n + page_size] for n in range(0, len(bug_ids), page_size)
|
|
593
|
+
]:
|
|
594
|
+
results.extend(
|
|
595
|
+
self.search_as(
|
|
596
|
+
{"id": ",".join(str(id) for id in bug_ids_chunk)},
|
|
597
|
+
bug_type,
|
|
598
|
+
)
|
|
599
|
+
)
|
|
600
|
+
return results
|
|
601
|
+
|
|
528
602
|
def bug_history(
|
|
529
603
|
self, bug_id: int, new_since: Optional[datetime] = None
|
|
530
604
|
) -> BugHistory:
|
|
@@ -532,9 +606,10 @@ class Bugzilla:
|
|
|
532
606
|
params = {}
|
|
533
607
|
if new_since is not None:
|
|
534
608
|
params["new_since"] = new_since.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
535
|
-
|
|
609
|
+
data = self.check_error(
|
|
536
610
|
self.request("GET", f"bug/{bug_id}/history", params=params)
|
|
537
611
|
)
|
|
612
|
+
query_result = BugsHistory.model_validate(data)
|
|
538
613
|
if query_result.faults:
|
|
539
614
|
raise BugzillaError(query_result.faults)
|
|
540
615
|
bugs = query_result.bugs
|
|
@@ -543,13 +618,15 @@ class Bugzilla:
|
|
|
543
618
|
assert len(bugs) == 1
|
|
544
619
|
return bugs[0]
|
|
545
620
|
|
|
546
|
-
def
|
|
621
|
+
def _search(
|
|
547
622
|
self,
|
|
623
|
+
bug_type: type[BugType],
|
|
548
624
|
query: QueryParams,
|
|
549
625
|
include_fields: Optional[list[str]] = None,
|
|
550
626
|
page_size: int = 100,
|
|
551
|
-
) -> list[
|
|
627
|
+
) -> list[BugType]:
|
|
552
628
|
"""Search for bugs using the bugzilla query API"""
|
|
629
|
+
|
|
553
630
|
query = {**query}
|
|
554
631
|
paginate = False
|
|
555
632
|
offset = 0
|
|
@@ -558,12 +635,12 @@ class Bugzilla:
|
|
|
558
635
|
query["offset"] = "0"
|
|
559
636
|
paginate = True
|
|
560
637
|
|
|
561
|
-
results: list[
|
|
638
|
+
results: list[BugType] = []
|
|
562
639
|
while True:
|
|
563
|
-
response = self.
|
|
564
|
-
"GET", "bug", params=query, include_fields=include_fields
|
|
640
|
+
response = self.check_error(
|
|
641
|
+
self.request("GET", "bug", params=query, include_fields=include_fields)
|
|
565
642
|
)
|
|
566
|
-
search_result =
|
|
643
|
+
search_result = bug_search_model(bug_type).model_validate(response)
|
|
567
644
|
if search_result.faults:
|
|
568
645
|
raise BugzillaError(search_result.faults)
|
|
569
646
|
if search_result.bugs is not None:
|
|
@@ -581,6 +658,20 @@ class Bugzilla:
|
|
|
581
658
|
|
|
582
659
|
return results
|
|
583
660
|
|
|
661
|
+
def search(
|
|
662
|
+
self,
|
|
663
|
+
query: QueryParams,
|
|
664
|
+
include_fields: Optional[list[str]] = None,
|
|
665
|
+
page_size: int = 100,
|
|
666
|
+
) -> list[Bug]:
|
|
667
|
+
return self._search(Bug, query, include_fields, page_size)
|
|
668
|
+
|
|
669
|
+
def search_as(
|
|
670
|
+
self, query: QueryParams, bug_type: type[BugType], page_size: int = 100
|
|
671
|
+
):
|
|
672
|
+
include_fields = list(bug_type.model_fields.keys())
|
|
673
|
+
return self._search(bug_type, query, include_fields, page_size)
|
|
674
|
+
|
|
584
675
|
def update_bugs(
|
|
585
676
|
self, update_params: BugUpdate, bug_id: Optional[int] = None
|
|
586
677
|
) -> list[BugUpdateResponse]:
|
|
@@ -600,7 +691,7 @@ class Bugzilla:
|
|
|
600
691
|
|
|
601
692
|
json_body = update_params.model_dump(exclude_none=True)
|
|
602
693
|
|
|
603
|
-
response = self.request("PUT", path, json_body=json_body)
|
|
694
|
+
response = self.check_error(self.request("PUT", path, json_body=json_body))
|
|
604
695
|
|
|
605
696
|
if self.config.allow_writes:
|
|
606
697
|
update_result = BugsUpdateResponse.model_validate(response)
|
|
@@ -632,7 +723,7 @@ class Bugzilla:
|
|
|
632
723
|
if not json_body.get("ids"):
|
|
633
724
|
json_body["ids"] = [bug_id]
|
|
634
725
|
|
|
635
|
-
response = self.request("POST", path, json_body=json_body)
|
|
726
|
+
response = self.check_error(self.request("POST", path, json_body=json_body))
|
|
636
727
|
|
|
637
728
|
if self.config.allow_writes:
|
|
638
729
|
create_result = AttachmentsCreateResponse.model_validate(response)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import pytest
|
|
2
|
+
from pydantic import BaseModel
|
|
2
3
|
|
|
3
4
|
from bugdantic import Bugzilla, BugzillaConfig
|
|
5
|
+
from bugdantic.bugzilla import BugComment
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
@pytest.fixture
|
|
@@ -26,7 +28,7 @@ def test_bug_history_full(bugzilla):
|
|
|
26
28
|
assert result.id == 1886129
|
|
27
29
|
|
|
28
30
|
|
|
29
|
-
def
|
|
31
|
+
def test_search_include_history(bugzilla):
|
|
30
32
|
bugs = [423488, 1749533]
|
|
31
33
|
result = bugzilla.search({"id": bugs}, include_fields=["id", "history"])
|
|
32
34
|
for expected_id, bug in zip(bugs, sorted(result, key=lambda x: x.id)):
|
|
@@ -43,3 +45,27 @@ def test_search_include_comments_and_attachments(bugzilla):
|
|
|
43
45
|
assert bug.id == expected_id
|
|
44
46
|
assert isinstance(bug.comments, list)
|
|
45
47
|
assert isinstance(bug.attachments, list)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_bug_as(bugzilla):
|
|
51
|
+
class BugData(BaseModel):
|
|
52
|
+
id: int
|
|
53
|
+
cf_user_story: str
|
|
54
|
+
|
|
55
|
+
result = bugzilla.bug_as(975444, BugData)
|
|
56
|
+
assert isinstance(result, BugData)
|
|
57
|
+
assert result.id == 975444
|
|
58
|
+
assert result.cf_user_story is not None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_search_as(bugzilla):
|
|
62
|
+
class BugData(BaseModel):
|
|
63
|
+
id: int
|
|
64
|
+
comments: list[BugComment]
|
|
65
|
+
|
|
66
|
+
bugs = [423488, 1749533]
|
|
67
|
+
result = bugzilla.search_as({"id": bugs}, BugData)
|
|
68
|
+
for expected_id, bug in zip(bugs, sorted(result, key=lambda x: x.id)):
|
|
69
|
+
assert isinstance(bug, BugData)
|
|
70
|
+
assert bug.id == expected_id
|
|
71
|
+
assert isinstance(bug.comments, list)
|
|
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
|
|
File without changes
|
|
File without changes
|