lorebase-client 0.3.1__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.
Files changed (22) hide show
  1. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/PKG-INFO +1 -1
  2. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/pyproject.toml +1 -1
  3. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/src/lorebase_client/__init__.py +1 -1
  4. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/src/lorebase_client/aclient.py +13 -1
  5. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/src/lorebase_client/client.py +13 -1
  6. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/src/lorebase_client/endpoints.py +2 -0
  7. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/src/lorebase_client/models.py +36 -1
  8. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/tests/test_contract.py +3 -0
  9. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/tests/test_requests.py +29 -0
  10. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/.gitignore +0 -0
  11. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/LICENSE +0 -0
  12. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/README.md +0 -0
  13. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/src/lorebase_client/config.py +0 -0
  14. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/src/lorebase_client/core.py +0 -0
  15. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/src/lorebase_client/errors.py +0 -0
  16. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/src/lorebase_client/py.typed +0 -0
  17. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/tests/integration/conftest.py +0 -0
  18. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/tests/integration/test_flow.py +0 -0
  19. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/tests/test_errors_and_config.py +0 -0
  20. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/tests/test_parity.py +0 -0
  21. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/tests/test_path_segments.py +0 -0
  22. {lorebase_client-0.3.1 → lorebase_client-0.4.0}/tests/test_trace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lorebase-client
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: Lorebase client: typed sync and async HTTP client for the Keeper API
5
5
  Project-URL: Homepage, https://github.com/aantenore/lorebase
6
6
  Project-URL: Documentation, https://aantenore.github.io/lorebase/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lorebase-client"
3
- version = "0.3.1"
3
+ version = "0.4.0"
4
4
  description = "Lorebase client: typed sync and async HTTP client for the Keeper API"
5
5
  requires-python = ">=3.12"
6
6
  readme = "README.md"
@@ -45,7 +45,7 @@ from lorebase_client.errors import (
45
45
  error_from_response,
46
46
  )
47
47
 
48
- __version__ = "0.3.1"
48
+ __version__ = "0.4.0"
49
49
 
50
50
  __all__ = [
51
51
  "ENV_API_KEY",
@@ -710,9 +710,21 @@ class Records(_Resource):
710
710
  at: str | None = None,
711
711
  links: bool = False,
712
712
  lineage: bool = False,
713
+ conflicts: bool = False,
713
714
  ) -> m.RecordResponse:
715
+ """One Record. `conflicts` adds `conflict_in_progress`: the open Conflicts about it
716
+ in Proposals into the branch read (branch head only)."""
714
717
  return await self._c.send(
715
- ep.get_record(domain, kind, key, branch=branch, at=at, links=links, lineage=lineage)
718
+ ep.get_record(
719
+ domain,
720
+ kind,
721
+ key,
722
+ branch=branch,
723
+ at=at,
724
+ links=links,
725
+ lineage=lineage,
726
+ conflicts=conflicts,
727
+ )
716
728
  )
717
729
 
718
730
  async def history(
@@ -721,9 +721,21 @@ class Records(_Resource):
721
721
  at: str | None = None,
722
722
  links: bool = False,
723
723
  lineage: bool = False,
724
+ conflicts: bool = False,
724
725
  ) -> m.RecordResponse:
726
+ """One Record. `conflicts` adds `conflict_in_progress`: the open Conflicts about it
727
+ in Proposals into the branch read (branch head only)."""
725
728
  return self._c.send(
726
- ep.get_record(domain, kind, key, branch=branch, at=at, links=links, lineage=lineage)
729
+ ep.get_record(
730
+ domain,
731
+ kind,
732
+ key,
733
+ branch=branch,
734
+ at=at,
735
+ links=links,
736
+ lineage=lineage,
737
+ conflicts=conflicts,
738
+ )
727
739
  )
728
740
 
729
741
  def history(
@@ -471,6 +471,7 @@ def get_record(
471
471
  at: str | None = None,
472
472
  links: bool = False,
473
473
  lineage: bool = False,
474
+ conflicts: bool = False,
474
475
  ) -> Call[m.RecordResponse]:
475
476
  return Call(
476
477
  "GET",
@@ -481,6 +482,7 @@ def get_record(
481
482
  "at": at,
482
483
  "links": links or None,
483
484
  "lineage": lineage or None,
485
+ "conflicts": conflicts or None,
484
486
  },
485
487
  safe=True,
486
488
  cacheable=True,
@@ -547,8 +547,22 @@ class Served(Model):
547
547
  served_from: ServedFrom
548
548
 
549
549
 
550
+ class ConflictInProgress(Model):
551
+ """A Conflict about a Record that waits for a decision in an open Proposal into the
552
+ branch read; `attribute` is None when the whole Record is contested."""
553
+
554
+ proposal_id: str
555
+ conflict_id: str
556
+ code: str
557
+ attribute: str | None = None
558
+ competing_hash: str | None = None
559
+
560
+
550
561
  class RecordResponse(Served):
562
+ """A Record. `conflict_in_progress` is set when the read asked for it (`conflicts`)."""
563
+
551
564
  record: RecordItem
565
+ conflict_in_progress: list[ConflictInProgress] | None = None
552
566
 
553
567
 
554
568
  class RecordPage(Served):
@@ -1155,14 +1169,35 @@ class Attempt(Model):
1155
1169
  auto_resolutions: list[dict[str, Any]]
1156
1170
 
1157
1171
 
1172
+ class SideProvenance(Model):
1173
+ """Who or what last changed the Attribute of a Conflict on one side, and when."""
1174
+
1175
+ provenance_id: str
1176
+ at: datetime
1177
+ actor: str | None = None
1178
+ origin: str | None = None
1179
+ source: str | None = None
1180
+ principal: str | None = None
1181
+ labels: list[str] = Field(default_factory=list)
1182
+
1183
+
1184
+ class ConflictProvenance(Model):
1185
+ """The provenance of the two competing sides; `None` for a side of unknown Lineage."""
1186
+
1187
+ ours: SideProvenance | None = None
1188
+ theirs: SideProvenance | None = None
1189
+
1190
+
1158
1191
  class ConflictItem(Model):
1159
- """A Conflict: `id`, `status` and the members the merge engine reports."""
1192
+ """A Conflict: `id`, `status` and the members the merge engine reports. `provenance` is
1193
+ set on Attribute Conflicts whose merge knew who changed a side."""
1160
1194
 
1161
1195
  model_config = ConfigDict(extra="allow")
1162
1196
 
1163
1197
  id: str
1164
1198
  status: ConflictStatus
1165
1199
  resolution: dict[str, Any] | None = None
1200
+ provenance: ConflictProvenance | None = None
1166
1201
 
1167
1202
 
1168
1203
  class Conflicts(Attempt):
@@ -64,6 +64,7 @@ RESPONSES: dict[type[m.Model], str] = {
64
64
  m.ExtractionEvidence: "ExtractionEvidenceOut",
65
65
  m.ValueLineage: "ValueLineageOut",
66
66
  m.RecordLineage: "RecordLineageOut",
67
+ m.ConflictInProgress: "ConflictInProgressOut",
67
68
  m.RecordResponse: "RecordResponse",
68
69
  m.RecordPage: "RecordPage",
69
70
  m.AggregateCount: "AggregateCount",
@@ -117,6 +118,8 @@ RESPONSES: dict[type[m.Model], str] = {
117
118
  m.Approval: "ApprovalOut",
118
119
  m.Resolution: "ResolutionOut",
119
120
  m.Attempt: "AttemptOut",
121
+ m.SideProvenance: "SideProvenanceOut",
122
+ m.ConflictProvenance: "ConflictProvenanceOut",
120
123
  m.ConflictItem: "ConflictOut",
121
124
  m.Conflicts: "ConflictsOut",
122
125
  m.Proposal: "lorebase_keeper__routes__proposals__ProposalOut",
@@ -590,6 +590,35 @@ RECORD = {
590
590
  }
591
591
 
592
592
 
593
+ def test_a_record_read_can_ask_for_pending_conflicts(
594
+ http: respx.MockRouter, sleeps: Sleeps
595
+ ) -> None:
596
+ pending = {
597
+ "proposal_id": "prp_1",
598
+ "conflict_id": "ab" * 32,
599
+ "code": "DV1",
600
+ "attribute": "risk_score",
601
+ "competing_hash": "cd" * 32,
602
+ }
603
+ route = http.get("/domains/supply/kinds/Part/records/P-1").mock(
604
+ side_effect=lambda request: httpx.Response(
605
+ 200,
606
+ json={**RECORD, "conflict_in_progress": [pending]}
607
+ if "conflicts" in request.url.params
608
+ else RECORD,
609
+ )
610
+ )
611
+ with make(sleeps) as client:
612
+ plain = client.records.get("supply", "Part", "P-1")
613
+ asked = client.records.get("supply", "Part", "P-1", conflicts=True)
614
+ assert "conflicts" not in route.calls[0].request.url.params
615
+ assert route.calls[1].request.url.params["conflicts"] == "true"
616
+ assert plain.conflict_in_progress is None
617
+ assert asked.conflict_in_progress is not None
618
+ [marker] = asked.conflict_in_progress
619
+ assert marker.model_dump() == pending
620
+
621
+
593
622
  def test_etag_cache_revalidates_and_serves_the_cached_body(
594
623
  http: respx.MockRouter, sleeps: Sleeps
595
624
  ) -> None:
File without changes