bugdantic 0.6.1__tar.gz → 0.6.2__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.6.1
3
+ Version: 0.6.2
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
@@ -9,6 +9,7 @@ from datetime import datetime
9
9
  from typing import (
10
10
  Any,
11
11
  Generic,
12
+ Literal,
12
13
  Optional,
13
14
  Self,
14
15
  TypeVar,
@@ -487,6 +488,100 @@ def comments_response_model(
487
488
  )
488
489
 
489
490
 
491
+ # Data models for getting products
492
+
493
+
494
+ class FlagType(BaseModel):
495
+ id: Optional[int] = None
496
+ name: Optional[str] = None
497
+ description: Optional[str] = None
498
+ cc_list: Optional[str] = None
499
+ sort_key: Optional[int] = None
500
+ is_active: Optional[bool] = None
501
+ is_requestable: Optional[bool] = None
502
+ is_requesteeble: Optional[bool] = None
503
+ is_multiplicable: Optional[bool] = None
504
+ grant_group: Optional[int] = None
505
+ request_group: Optional[int] = None
506
+
507
+
508
+ class ComponentFlagTypes(BaseModel):
509
+ bug: Optional[list[FlagType]] = None
510
+ attachment: Optional[list[FlagType]] = None
511
+
512
+
513
+ class ProductComponent(BaseModel):
514
+ id: Optional[int] = None
515
+ name: Optional[str] = None
516
+ description: Optional[str] = None
517
+ default_assigned_to: Optional[str] = None
518
+ default_qa_contact: Optional[str] = None
519
+ triage_owner: Optional[str] = None
520
+ team_name: Optional[str] = None
521
+ sort_key: Optional[int] = None
522
+ is_active: Optional[bool] = None
523
+ default_bug_type: Optional[str] = None
524
+ flag_types: Optional[ComponentFlagTypes] = None
525
+
526
+
527
+ class ProductVersion(BaseModel):
528
+ id: Optional[int] = None
529
+ name: Optional[str] = None
530
+ sort_key: Optional[int] = None
531
+ is_active: Optional[bool] = None
532
+
533
+
534
+ class ProductMilestone(BaseModel):
535
+ id: Optional[int] = None
536
+ name: Optional[str] = None
537
+ sort_key: Optional[int] = None
538
+ is_active: Optional[bool] = None
539
+
540
+
541
+ class Product(BaseModel):
542
+ id: Optional[int] = None
543
+ name: Optional[str] = None
544
+ description: Optional[str] = None
545
+ is_active: Optional[bool] = None
546
+ default_bug_type: Optional[str] = None
547
+ default_milestone: Optional[str] = None
548
+ default_version: Optional[str] = None
549
+ has_unconfirmed: Optional[bool] = None
550
+ classification: Optional[str] = None
551
+ components: Optional[list[ProductComponent]] = None
552
+ versions: Optional[list[ProductVersion]] = None
553
+ milestones: Optional[list[ProductMilestone]] = None
554
+
555
+ # BMO specific fields
556
+ default_platform: Optional[str] = None
557
+ default_op_sys: Optional[str] = None
558
+ default_security_group: Optional[str] = None
559
+
560
+ model_config = ConfigDict(extra="allow")
561
+
562
+ def to_dict(self) -> dict[str, Any]:
563
+ return self.model_dump(exclude_unset=True)
564
+
565
+
566
+ ProductAccess = Literal["accessible", "selectable", "enterable"]
567
+
568
+
569
+ ProductType = TypeVar("ProductType", bound=BaseModel)
570
+
571
+
572
+ class ProductsResponseGeneric(BaseModel, Generic[ProductType]):
573
+ products: list[ProductType] = []
574
+
575
+
576
+ def products_response_model(
577
+ product_type: type[ProductType],
578
+ ) -> type[ProductsResponseGeneric[ProductType]]:
579
+ return cast(
580
+ type[ProductsResponseGeneric[ProductType]],
581
+ ProductsResponseGeneric.__class_getitem__(product_type),
582
+ )
583
+
584
+
490
585
  class ErrorResponse(BaseModel):
491
586
  code: int
492
587
  message: str
@@ -770,6 +865,60 @@ class Bugzilla:
770
865
  comment_ids, comment_type, include_fields, exclude_fields, page_size
771
866
  )
772
867
 
868
+ def _products(
869
+ self,
870
+ product_type: type[ProductType],
871
+ ids: Optional[Sequence[int]] = None,
872
+ names: Optional[Sequence[str]] = None,
873
+ access: Sequence[ProductAccess] = ("accessible",),
874
+ include_fields: Optional[list[str]] = None,
875
+ exclude_fields: Optional[list[str]] = None,
876
+ ) -> list[ProductType]:
877
+ """Get products specified by id, name, or access type.
878
+
879
+ Products that don't exist or aren't accessible to the user are
880
+ silently omitted."""
881
+ params: dict[str, Sequence[QueryValue]] = {"type": list(access)}
882
+ if ids is not None:
883
+ params["ids"] = [str(item) for item in ids]
884
+ if names is not None:
885
+ params["names"] = list(names)
886
+ response = self.check_error(
887
+ self.request(
888
+ "GET",
889
+ "product",
890
+ params=params,
891
+ include_fields=include_fields,
892
+ exclude_fields=exclude_fields,
893
+ )
894
+ )
895
+ return products_response_model(product_type).model_validate(response).products
896
+
897
+ def products(
898
+ self,
899
+ ids: Optional[Sequence[int]] = None,
900
+ names: Optional[Sequence[str]] = None,
901
+ access: Sequence[ProductAccess] = ("accessible",),
902
+ include_fields: Optional[list[str]] = None,
903
+ exclude_fields: Optional[list[str]] = None,
904
+ ) -> list[Product]:
905
+ return self._products(
906
+ Product, ids, names, access, include_fields, exclude_fields
907
+ )
908
+
909
+ def products_as(
910
+ self,
911
+ product_type: type[ProductType],
912
+ ids: Optional[Sequence[int]] = None,
913
+ names: Optional[Sequence[str]] = None,
914
+ access: Sequence[ProductAccess] = ("accessible",),
915
+ exclude_fields: Optional[list[str]] = None,
916
+ ) -> list[ProductType]:
917
+ include_fields = model_field_names(product_type)
918
+ return self._products(
919
+ product_type, ids, names, access, include_fields, exclude_fields
920
+ )
921
+
773
922
  def bug_history(
774
923
  self, bug_id: int, new_since: Optional[datetime] = None
775
924
  ) -> BugHistory:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bugdantic
3
- Version: 0.6.1
3
+ Version: 0.6.2
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
@@ -5,7 +5,7 @@ authors = [
5
5
  {name = "James Graham", email = "james@hoppipolla.co.uk"}
6
6
  ]
7
7
 
8
- version = "0.6.1"
8
+ version = "0.6.2"
9
9
  requires-python = ">=3.11"
10
10
  license = "MPL-2.0"
11
11
  classifiers = [
@@ -4,7 +4,7 @@ import pytest
4
4
  from pydantic import BaseModel, Field
5
5
 
6
6
  from bugdantic import Bugzilla, BugzillaConfig, BugzillaResponseError
7
- from bugdantic.bugzilla import BugComment
7
+ from bugdantic.bugzilla import BugComment, Product
8
8
 
9
9
 
10
10
  @pytest.fixture
@@ -156,3 +156,49 @@ def test_error_fails_whole_request(bugzilla):
156
156
  with pytest.raises(BugzillaResponseError) as exc_info:
157
157
  bugzilla.comments([18109498, 0, 18109500])
158
158
  assert exc_info.value.response_data.code == 111
159
+
160
+
161
+ def test_products_by_name(bugzilla):
162
+ result = bugzilla.products(names=["Web Compatibility"])
163
+ assert len(result) == 1
164
+ product = result[0]
165
+ assert isinstance(product, Product)
166
+ assert product.name == "Web Compatibility"
167
+ assert product.components
168
+ assert any(component.name == "Site Reports" for component in product.components)
169
+
170
+
171
+ def test_products_by_id(bugzilla):
172
+ [by_name] = bugzilla.products(names=["Web Compatibility"], include_fields=["id"])
173
+ result = bugzilla.products(ids=[by_name.id], include_fields=["id", "name"])
174
+ assert [(item.id, item.name) for item in result] == [
175
+ (by_name.id, "Web Compatibility")
176
+ ]
177
+
178
+
179
+ def test_products_missing(bugzilla):
180
+ assert bugzilla.products(names=["This product does not exist"]) == []
181
+
182
+
183
+ def test_products_as(bugzilla):
184
+ class ComponentName(BaseModel):
185
+ name: str
186
+
187
+ class ProductComponents(BaseModel):
188
+ name: str
189
+ components: list[ComponentName]
190
+
191
+ result = bugzilla.products_as(
192
+ ProductComponents, names=["Web Compatibility", "Core"]
193
+ )
194
+ assert all(isinstance(item, ProductComponents) for item in result)
195
+ assert {item.name for item in result} == {"Web Compatibility", "Core"}
196
+ assert all(item.components for item in result)
197
+
198
+
199
+ def test_products_access(bugzilla):
200
+ class ProductName(BaseModel):
201
+ name: str
202
+
203
+ result = bugzilla.products_as(ProductName, access=["enterable"])
204
+ assert "Web Compatibility" in {item.name for item in result}
File without changes
File without changes
File without changes
File without changes