bugdantic 0.3.0__tar.gz → 0.4.1__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.1}/PKG-INFO +1 -1
- {bugdantic-0.3.0 → bugdantic-0.4.1}/bugdantic/bugzilla.py +112 -14
- {bugdantic-0.3.0 → bugdantic-0.4.1}/bugdantic.egg-info/PKG-INFO +1 -1
- {bugdantic-0.3.0 → bugdantic-0.4.1}/pyproject.toml +1 -1
- {bugdantic-0.3.0 → bugdantic-0.4.1}/test/test_bugzilla.py +38 -1
- {bugdantic-0.3.0 → bugdantic-0.4.1}/LICENSE +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.1}/README.md +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.1}/bugdantic/__init__.py +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.1}/bugdantic/py.typed +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.1}/bugdantic/userstory.py +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.1}/bugdantic.egg-info/SOURCES.txt +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.1}/bugdantic.egg-info/dependency_links.txt +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.1}/bugdantic.egg-info/requires.txt +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.1}/bugdantic.egg-info/top_level.txt +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.1}/setup.cfg +0 -0
- {bugdantic-0.3.0 → bugdantic-0.4.1}/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,27 @@ 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
|
+
|
|
457
|
+
def model_field_names(model: type[BaseModel]) -> list[str]:
|
|
458
|
+
return [
|
|
459
|
+
field.alias if field.alias else name
|
|
460
|
+
for name, field in model.model_fields.items()
|
|
461
|
+
]
|
|
462
|
+
|
|
463
|
+
|
|
425
464
|
class Bugzilla:
|
|
426
465
|
def __init__(self, config: BugzillaConfig):
|
|
427
466
|
self.config = config
|
|
@@ -491,13 +530,27 @@ class Bugzilla:
|
|
|
491
530
|
""")
|
|
492
531
|
return {}
|
|
493
532
|
|
|
494
|
-
def
|
|
495
|
-
|
|
496
|
-
|
|
533
|
+
def check_error(self, data: Mapping[str, Json]) -> Mapping[str, Json]:
|
|
534
|
+
try:
|
|
535
|
+
err = ErrorResponse.model_validate(data)
|
|
536
|
+
if err.error:
|
|
537
|
+
raise BugzillaError(err.message)
|
|
538
|
+
except pydantic.ValidationError:
|
|
539
|
+
pass
|
|
540
|
+
return data
|
|
541
|
+
|
|
542
|
+
def _bug(
|
|
543
|
+
self,
|
|
544
|
+
bug_id: int,
|
|
545
|
+
bug_type: type[BugType],
|
|
546
|
+
include_fields: Optional[list[str]] = None,
|
|
547
|
+
) -> Optional[BugType]:
|
|
497
548
|
"""Get a single bug specified by id"""
|
|
498
|
-
|
|
549
|
+
|
|
550
|
+
data = self.check_error(
|
|
499
551
|
self.request("GET", f"bug/{bug_id}", include_fields=include_fields)
|
|
500
552
|
)
|
|
553
|
+
search_result = bug_search_model(bug_type).model_validate(data)
|
|
501
554
|
if search_result.faults:
|
|
502
555
|
raise BugzillaError(search_result.faults)
|
|
503
556
|
bugs = search_result.bugs
|
|
@@ -506,6 +559,15 @@ class Bugzilla:
|
|
|
506
559
|
assert len(bugs) == 1
|
|
507
560
|
return bugs[0]
|
|
508
561
|
|
|
562
|
+
def bug(
|
|
563
|
+
self, bug_id: int, include_fields: Optional[list[str]] = None
|
|
564
|
+
) -> Optional[Bug]:
|
|
565
|
+
return self._bug(bug_id, Bug, include_fields)
|
|
566
|
+
|
|
567
|
+
def bug_as(self, bug_id: int, bug_type: type[BugType]) -> Optional[BugType]:
|
|
568
|
+
include_fields = model_field_names(bug_type)
|
|
569
|
+
return self._bug(bug_id, bug_type, include_fields)
|
|
570
|
+
|
|
509
571
|
def bugs(
|
|
510
572
|
self,
|
|
511
573
|
bug_ids: Sequence[int],
|
|
@@ -525,6 +587,25 @@ class Bugzilla:
|
|
|
525
587
|
)
|
|
526
588
|
return results
|
|
527
589
|
|
|
590
|
+
def bugs_as(
|
|
591
|
+
self,
|
|
592
|
+
bug_ids: Sequence[int],
|
|
593
|
+
bug_type: type[BugType],
|
|
594
|
+
page_size: int = 100,
|
|
595
|
+
) -> list[Bug]:
|
|
596
|
+
"""Get multiple bugs specified by id"""
|
|
597
|
+
results: list[Bug] = []
|
|
598
|
+
for bug_ids_chunk in [
|
|
599
|
+
bug_ids[n : n + page_size] for n in range(0, len(bug_ids), page_size)
|
|
600
|
+
]:
|
|
601
|
+
results.extend(
|
|
602
|
+
self.search_as(
|
|
603
|
+
{"id": ",".join(str(id) for id in bug_ids_chunk)},
|
|
604
|
+
bug_type,
|
|
605
|
+
)
|
|
606
|
+
)
|
|
607
|
+
return results
|
|
608
|
+
|
|
528
609
|
def bug_history(
|
|
529
610
|
self, bug_id: int, new_since: Optional[datetime] = None
|
|
530
611
|
) -> BugHistory:
|
|
@@ -532,9 +613,10 @@ class Bugzilla:
|
|
|
532
613
|
params = {}
|
|
533
614
|
if new_since is not None:
|
|
534
615
|
params["new_since"] = new_since.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
535
|
-
|
|
616
|
+
data = self.check_error(
|
|
536
617
|
self.request("GET", f"bug/{bug_id}/history", params=params)
|
|
537
618
|
)
|
|
619
|
+
query_result = BugsHistory.model_validate(data)
|
|
538
620
|
if query_result.faults:
|
|
539
621
|
raise BugzillaError(query_result.faults)
|
|
540
622
|
bugs = query_result.bugs
|
|
@@ -543,13 +625,15 @@ class Bugzilla:
|
|
|
543
625
|
assert len(bugs) == 1
|
|
544
626
|
return bugs[0]
|
|
545
627
|
|
|
546
|
-
def
|
|
628
|
+
def _search(
|
|
547
629
|
self,
|
|
630
|
+
bug_type: type[BugType],
|
|
548
631
|
query: QueryParams,
|
|
549
632
|
include_fields: Optional[list[str]] = None,
|
|
550
633
|
page_size: int = 100,
|
|
551
|
-
) -> list[
|
|
634
|
+
) -> list[BugType]:
|
|
552
635
|
"""Search for bugs using the bugzilla query API"""
|
|
636
|
+
|
|
553
637
|
query = {**query}
|
|
554
638
|
paginate = False
|
|
555
639
|
offset = 0
|
|
@@ -558,12 +642,12 @@ class Bugzilla:
|
|
|
558
642
|
query["offset"] = "0"
|
|
559
643
|
paginate = True
|
|
560
644
|
|
|
561
|
-
results: list[
|
|
645
|
+
results: list[BugType] = []
|
|
562
646
|
while True:
|
|
563
|
-
response = self.
|
|
564
|
-
"GET", "bug", params=query, include_fields=include_fields
|
|
647
|
+
response = self.check_error(
|
|
648
|
+
self.request("GET", "bug", params=query, include_fields=include_fields)
|
|
565
649
|
)
|
|
566
|
-
search_result =
|
|
650
|
+
search_result = bug_search_model(bug_type).model_validate(response)
|
|
567
651
|
if search_result.faults:
|
|
568
652
|
raise BugzillaError(search_result.faults)
|
|
569
653
|
if search_result.bugs is not None:
|
|
@@ -581,6 +665,20 @@ class Bugzilla:
|
|
|
581
665
|
|
|
582
666
|
return results
|
|
583
667
|
|
|
668
|
+
def search(
|
|
669
|
+
self,
|
|
670
|
+
query: QueryParams,
|
|
671
|
+
include_fields: Optional[list[str]] = None,
|
|
672
|
+
page_size: int = 100,
|
|
673
|
+
) -> list[Bug]:
|
|
674
|
+
return self._search(Bug, query, include_fields, page_size)
|
|
675
|
+
|
|
676
|
+
def search_as(
|
|
677
|
+
self, query: QueryParams, bug_type: type[BugType], page_size: int = 100
|
|
678
|
+
):
|
|
679
|
+
include_fields = model_field_names(bug_type)
|
|
680
|
+
return self._search(bug_type, query, include_fields, page_size)
|
|
681
|
+
|
|
584
682
|
def update_bugs(
|
|
585
683
|
self, update_params: BugUpdate, bug_id: Optional[int] = None
|
|
586
684
|
) -> list[BugUpdateResponse]:
|
|
@@ -600,7 +698,7 @@ class Bugzilla:
|
|
|
600
698
|
|
|
601
699
|
json_body = update_params.model_dump(exclude_none=True)
|
|
602
700
|
|
|
603
|
-
response = self.request("PUT", path, json_body=json_body)
|
|
701
|
+
response = self.check_error(self.request("PUT", path, json_body=json_body))
|
|
604
702
|
|
|
605
703
|
if self.config.allow_writes:
|
|
606
704
|
update_result = BugsUpdateResponse.model_validate(response)
|
|
@@ -632,7 +730,7 @@ class Bugzilla:
|
|
|
632
730
|
if not json_body.get("ids"):
|
|
633
731
|
json_body["ids"] = [bug_id]
|
|
634
732
|
|
|
635
|
-
response = self.request("POST", path, json_body=json_body)
|
|
733
|
+
response = self.check_error(self.request("POST", path, json_body=json_body))
|
|
636
734
|
|
|
637
735
|
if self.config.allow_writes:
|
|
638
736
|
create_result = AttachmentsCreateResponse.model_validate(response)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import pytest
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
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,38 @@ 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)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_bug_as_alias(bugzilla):
|
|
75
|
+
class BugData(BaseModel):
|
|
76
|
+
id: int
|
|
77
|
+
user_story: str = Field(alias="cf_user_story")
|
|
78
|
+
|
|
79
|
+
result = bugzilla.bug_as(975444, BugData)
|
|
80
|
+
assert isinstance(result, BugData)
|
|
81
|
+
assert result.id == 975444
|
|
82
|
+
assert result.user_story is not None
|
|
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
|