unique_toolkit 2026.34.0.dev0__py3-none-any.whl → 2026.34.0.dev1__py3-none-any.whl

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,7 +1,8 @@
1
1
  import asyncio
2
2
  import logging
3
3
  import time
4
- from typing import Any, cast
4
+ from datetime import datetime
5
+ from typing import Any, NamedTuple, cast
5
6
 
6
7
  from typing_extensions import deprecated
7
8
  from unique_sdk import (
@@ -49,6 +50,56 @@ def _sheet_batch_cells_include_row_metadata_from_api(
49
50
  return "rowMetadata" in first and isinstance(rm, list)
50
51
 
51
52
 
53
+ def _artifact_timestamp(value: str) -> datetime:
54
+ """Parse an artifact ISO-8601 timestamp (e.g. ``2026-07-31T16:26:36.344Z``)."""
55
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
56
+
57
+
58
+ class _ArtifactBaseline(NamedTuple):
59
+ """What a sheet's artifacts of one type looked like just before an export of
60
+ that type was triggered.
61
+
62
+ ``ids`` are the records of the type that already existed; ``latest`` is
63
+ their newest ``updated_at``. Two signals because either can carry the
64
+ result: a finished report normally arrives as a new record (the trigger's
65
+ nameless IN_PROGRESS marker and the named report are separate rows), but a
66
+ re-export can also refresh an existing record in place.
67
+
68
+ Kept per artifact type so that triggering one type does not fence out an
69
+ already-completed export of another type.
70
+
71
+ Mirrors the ``_ArtifactBaseline`` used by the SDK CLI's export wait.
72
+ """
73
+
74
+ ids: frozenset[str]
75
+ latest: datetime | None
76
+
77
+
78
+ def _snapshot_artifacts(
79
+ artifacts: list[MagicTableArtifact],
80
+ artifact_types: list[MagicTableArtifactType],
81
+ ) -> dict[MagicTableArtifactType, _ArtifactBaseline]:
82
+ baselines: dict[MagicTableArtifactType, _ArtifactBaseline] = {}
83
+ for artifact_type in artifact_types:
84
+ records = [a for a in artifacts if a.artifact_type == artifact_type]
85
+ timestamps = [_artifact_timestamp(a.updated_at) for a in records]
86
+ baselines[artifact_type] = _ArtifactBaseline(
87
+ ids=frozenset(a.id for a in records),
88
+ latest=max(timestamps) if timestamps else None,
89
+ )
90
+ return baselines
91
+
92
+
93
+ def _is_fresh(artifact: MagicTableArtifact, baseline: _ArtifactBaseline) -> bool:
94
+ """Whether *artifact* came out of a generation triggered after *baseline*."""
95
+ if artifact.id not in baseline.ids:
96
+ return True
97
+ return (
98
+ baseline.latest is not None
99
+ and _artifact_timestamp(artifact.updated_at) > baseline.latest
100
+ )
101
+
102
+
52
103
  class LockedAgenticTableError(Exception):
53
104
  pass
54
105
 
@@ -88,6 +139,9 @@ class AgenticTableService:
88
139
  self.table_id = table_id
89
140
  self.logger = logger
90
141
  self.created_sheet: CreatedMagicTableSheet | None = None
142
+ # Per-type snapshots taken by generate_artifacts so wait_for_artifacts can
143
+ # tell a new export apart from records left by earlier generations.
144
+ self._artifact_baselines: dict[MagicTableArtifactType, _ArtifactBaseline] = {}
91
145
 
92
146
  @classmethod
93
147
  async def create_sheet(
@@ -634,9 +688,17 @@ class AgenticTableService:
634
688
  (or poll ``list_artifacts``) to detect completion, then download the artifact's
635
689
  ``content_id`` via the Content API.
636
690
 
691
+ Snapshots the sheet's artifacts of the requested types before
692
+ triggering, so a following ``wait_for_artifacts`` on this instance can
693
+ tell the new export apart from records left over from earlier
694
+ generations. Only the requested types' snapshots are replaced; a
695
+ baseline armed by an earlier trigger for another type is kept, so its
696
+ completed export stays recognisable.
697
+
637
698
  Raises:
638
699
  Exception: If the API reports a non-success status.
639
700
  """
701
+ baselines = _snapshot_artifacts(await self.list_artifacts(), artifact_types)
640
702
  result = await AgenticTable.generate_artifact(
641
703
  user_id=self._user_id,
642
704
  company_id=self._company_id,
@@ -647,6 +709,7 @@ class AgenticTableService:
647
709
  raise Exception(
648
710
  result.get("message") or "Failed to trigger artifact generation"
649
711
  )
712
+ self._artifact_baselines.update(baselines)
650
713
 
651
714
  async def rerun_row(self, row_order: int) -> None:
652
715
  """Re-run the agent for a single row (`POST .../row/{rowOrder}/rerun`).
@@ -798,15 +861,26 @@ class AgenticTableService:
798
861
  ) -> list[MagicTableArtifact]:
799
862
  """Wait until artifacts of the given types are DONE, by polling ``list_artifacts``.
800
863
 
801
- Artifact records are upserted per type: triggering an export flips the record
802
- of that type to IN_PROGRESS and back to DONE when the file is ready. A
803
- terminal state is accepted only after IN_PROGRESS has been observed for
804
- that artifact type, so a result left over from an earlier generation is
805
- not mistaken for the newly triggered one.
806
-
807
- Because the API exposes only the current artifact state, a complete
808
- IN_PROGRESS-to-terminal transition between two polls cannot be observed.
809
- Keep ``poll_interval`` shorter than the expected minimum generation time.
864
+ A sheet can hold several records of the same type: the export trigger
865
+ records a nameless IN_PROGRESS marker that is not always the record the
866
+ worker completes, so a stale IN_PROGRESS row can sit next to the DONE
867
+ result, and reports from earlier generations remain listed. Two fences
868
+ keep the wait from mistaking leftovers for the new export:
869
+
870
+ - For a type that ``generate_artifacts`` was called for on this
871
+ instance, only records that are *fresh* relative to that trigger's
872
+ snapshot count: a record id that did not exist before, or an
873
+ ``updated_at`` newer than the snapshot. This also accepts exports
874
+ that finish before the first poll.
875
+ - For a type without a snapshot (export triggered elsewhere), a
876
+ terminal state is accepted only after IN_PROGRESS has been observed
877
+ for that type, and the newest record (by ``updated_at``) decides the
878
+ state. This cannot distinguish a result of an earlier generation
879
+ whose trigger marker is still IN_PROGRESS — prefer triggering and
880
+ waiting on the same service instance.
881
+
882
+ Because the API exposes only the current artifact state, keep
883
+ ``poll_interval`` shorter than the expected minimum generation time.
810
884
 
811
885
  Args:
812
886
  artifact_types: The artifact types to wait for (as passed to ``generate_artifacts``).
@@ -821,22 +895,41 @@ class AgenticTableService:
821
895
  AgenticTableRunTimeoutError: Not all artifacts were DONE within ``timeout``.
822
896
  """
823
897
  wanted = set(artifact_types)
898
+ baselines = {
899
+ artifact_type: baseline
900
+ for artifact_type, baseline in self._artifact_baselines.items()
901
+ if artifact_type in wanted
902
+ }
824
903
  started: set[MagicTableArtifactType] = set()
825
904
  deadline = time.monotonic() + timeout
826
905
  while True:
827
906
  artifacts = await self.list_artifacts()
828
- current = {
829
- a.artifact_type: a for a in artifacts if a.artifact_type in wanted
907
+ relevant = [a for a in artifacts if a.artifact_type in wanted]
908
+ candidates: list[MagicTableArtifact] = []
909
+ for artifact in relevant:
910
+ baseline = baselines.get(artifact.artifact_type)
911
+ if baseline is None:
912
+ candidates.append(artifact)
913
+ if artifact.artifact_state == MagicTableArtifactState.IN_PROGRESS:
914
+ started.add(artifact.artifact_type)
915
+ elif _is_fresh(artifact, baseline):
916
+ candidates.append(artifact)
917
+ gate = started | {
918
+ a.artifact_type for a in candidates if a.artifact_type in baselines
830
919
  }
831
- started.update(
832
- artifact_type
833
- for artifact_type, artifact in current.items()
834
- if artifact.artifact_state == MagicTableArtifactState.IN_PROGRESS
835
- )
920
+ # Newest record per type decides the state; older duplicates (e.g. the
921
+ # stale nameless IN_PROGRESS row from the export trigger) are ignored.
922
+ current: dict[MagicTableArtifactType, MagicTableArtifact] = {}
923
+ for artifact in candidates:
924
+ newest = current.get(artifact.artifact_type)
925
+ if newest is None or _artifact_timestamp(
926
+ artifact.updated_at
927
+ ) > _artifact_timestamp(newest.updated_at):
928
+ current[artifact.artifact_type] = artifact
836
929
  failed = [
837
930
  artifact
838
931
  for artifact_type, artifact in current.items()
839
- if artifact_type in started
932
+ if artifact_type in gate
840
933
  and artifact.artifact_state == MagicTableArtifactState.ERROR
841
934
  ]
842
935
  if failed:
@@ -847,7 +940,7 @@ class AgenticTableService:
847
940
  done = {
848
941
  artifact_type: artifact
849
942
  for artifact_type, artifact in current.items()
850
- if artifact_type in started
943
+ if artifact_type in gate
851
944
  and artifact.artifact_state == MagicTableArtifactState.DONE
852
945
  }
853
946
  if wanted <= set(done.keys()):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-toolkit
3
- Version: 2026.34.0.dev0
3
+ Version: 2026.34.0.dev1
4
4
  Summary:
5
5
  Author: Cedric Klinkert, Martin Fadler, Sadique Sheik, Fabian Schläpfer, Pascal Hauri, Ejiroghene Oki-Peter, Rami Azouz, Ricardo Visini, Sanzio Monti
6
6
  Author-email: Cedric Klinkert <cedric.klinkert@unique.ch>, Martin Fadler <martin.fadler@unique.ch>, Sadique Sheik <sadique@unique.ch>, Fabian Schläpfer <fabian@unique.ch>, Pascal Hauri <pascal@unique.ch>, Ejiroghene Oki-Peter <oki-peter.ejiroghene@unique.ai>, Rami Azouz <rami.ext@unique.ch>, Ricardo Visini <ricardo.visini@unique.ch>, Sanzio Monti <sanzio.monti@unique.ai>
@@ -238,7 +238,7 @@ unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py,sha256=u
238
238
  unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py,sha256=EA8iVvb3L91OFk2XMbGcFuhe2etqm3Sx9QCYDGiOSOM,6995
239
239
  unique_toolkit/agentic_table/__init__.py,sha256=4WenUtjX3IPA88ljNAkfcckVMy9P8jUHR7LnaoLx6ok,2240
240
240
  unique_toolkit/agentic_table/schemas.py,sha256=GobP1gcWSiwX96wTG7c-Su2NoWBy191HWGXY5bB2_qU,14161
241
- unique_toolkit/agentic_table/service.py,sha256=cS-tPfePeZpDjvI9hgCIZnA11fxprIfeFNFfZI6VFJo,33722
241
+ unique_toolkit/agentic_table/service.py,sha256=WOX47T5dHj7yvNWCXEKXccVx5Kb-q-jADHSbU5luwxE,38298
242
242
  unique_toolkit/agentic_table/synthetic_chat_event.py,sha256=MylvYuhx5CVt2DbPetzV0sR8GlkL218DmNFi7A6RYVs,2373
243
243
  unique_toolkit/app/__init__.py,sha256=qear5ErW3smLDATxtnUsgkX3f177zsn65ixVW8T0JVE,1641
244
244
  unique_toolkit/app/chat_event_filter_options_settings.py,sha256=W0Ydio1UeED5_xqaTzbcGzK2tH4EpA1WHUzdF-iWpqI,364
@@ -419,6 +419,6 @@ unique_toolkit/short_term_memory/service.py,sha256=2JMBEBn6GoyK-a0xvdcWpPft-3C74
419
419
  unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
420
420
  unique_toolkit/smart_rules/compile.py,sha256=vRe0aMJWuVIK-VsqomgxQImzke7MDmwgSxxJfYCa7zw,1386
421
421
  unique_toolkit/test_utilities/events.py,sha256=FLrhMh0A_mQutAUjoD0FdMcVQ4Gq31r35OVGzRjvLro,6760
422
- unique_toolkit-2026.34.0.dev0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
423
- unique_toolkit-2026.34.0.dev0.dist-info/METADATA,sha256=Lyc4K9HylzXIg01YxNdOQQPeiub11RlYO3_5HkDergo,7494
424
- unique_toolkit-2026.34.0.dev0.dist-info/RECORD,,
422
+ unique_toolkit-2026.34.0.dev1.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
423
+ unique_toolkit-2026.34.0.dev1.dist-info/METADATA,sha256=tQFYjPfy4v1-MW-z3PRbkJRbpb_yzr_kcVNGtSCEYe4,7494
424
+ unique_toolkit-2026.34.0.dev1.dist-info/RECORD,,