weflow-sdk 0.9.0__tar.gz → 0.9.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.
Files changed (68) hide show
  1. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/PKG-INFO +14 -1
  2. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/README.md +13 -0
  3. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/pyproject.toml +1 -1
  4. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/__init__.py +2 -0
  5. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/client.py +269 -10
  6. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/spec.json +39 -1
  7. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/__init__.py +3 -1
  8. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/api/default_api.py +1 -1
  9. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/api_client.py +1 -1
  10. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/configuration.py +2 -2
  11. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/exceptions.py +1 -1
  12. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/__init__.py +2 -1
  13. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/account_conflict.py +9 -3
  14. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/account_deregistered.py +1 -1
  15. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/account_not_registered.py +1 -1
  16. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/account_phase.py +1 -1
  17. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/account_registered.py +9 -3
  18. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/account_state_view.py +8 -2
  19. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/account_status.py +1 -1
  20. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/account_wxid_mismatch.py +1 -1
  21. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/accounts_list.py +1 -1
  22. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/chatlab_header.py +1 -1
  23. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/chatlab_member.py +1 -1
  24. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/chatlab_message.py +1 -1
  25. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/chatlab_messages.py +1 -1
  26. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/chatlab_meta.py +1 -1
  27. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/contact.py +1 -1
  28. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/contacts.py +1 -1
  29. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/delete_api_v1_accounts_wxid200_response.py +1 -1
  30. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/event_media.py +1 -1
  31. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/event_new.py +1 -1
  32. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/event_revoke.py +1 -1
  33. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/event_sync.py +1 -1
  34. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/group_member.py +1 -1
  35. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/group_members.py +1 -1
  36. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/health.py +1 -1
  37. weflow_sdk-0.9.2/src/weflow_sdk/generated/weflow_sdk/models/index_progress_dto.py +91 -0
  38. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/media_brief.py +1 -1
  39. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/media_envelope.py +1 -1
  40. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/media_object.py +1 -1
  41. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/message_native.py +1 -1
  42. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/messages_native.py +1 -1
  43. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/notification_frame.py +1 -1
  44. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/page.py +1 -1
  45. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/post_api_v1_accounts200_response.py +1 -1
  46. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/pull_envelope.py +1 -1
  47. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/pull_message.py +1 -1
  48. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/pull_sync.py +1 -1
  49. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/quote.py +1 -1
  50. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/session_chatlab.py +1 -1
  51. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/session_native.py +1 -1
  52. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/sessions_chatlab.py +1 -1
  53. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/sessions_native.py +1 -1
  54. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/sync_frame.py +1 -1
  55. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/sync_result.py +1 -1
  56. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/watermark_entry.py +1 -1
  57. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/models/watermark_value.py +1 -1
  58. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/rest.py +1 -1
  59. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/tests/test_behavior.py +122 -7
  60. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/.gitignore +0 -0
  61. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/LICENSE +0 -0
  62. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/openapitools.json +0 -0
  63. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/scripts/regen.py +0 -0
  64. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/LICENSE +0 -0
  65. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/api/__init__.py +0 -0
  66. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/api_response.py +0 -0
  67. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/generated/weflow_sdk/py.typed +0 -0
  68. {weflow_sdk-0.9.0 → weflow_sdk-0.9.2}/src/weflow_sdk/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: weflow-sdk
3
- Version: 0.9.0
3
+ Version: 0.9.2
4
4
  Summary: Typed Python client for weflow-server's HTTP API (generated models + handwritten behavior layer)
5
5
  Project-URL: Homepage, https://github.com/ren-yc/weflow-server
6
6
  Project-URL: Repository, https://github.com/ren-yc/weflow-server
@@ -50,6 +50,19 @@ async def main() -> None:
50
50
  asyncio.run(main())
51
51
  ```
52
52
 
53
+ Need synchronous code? `SyncClient` mirrors `Client` method-for-method and drives its own
54
+ private event-loop thread — it neither hijacks nor requires a loop in your thread:
55
+
56
+ ```python
57
+ from weflow_sdk import SyncClient
58
+
59
+ with SyncClient(base_url="http://127.0.0.1:5033", token="your-token") as client:
60
+ client.health()
61
+ sessions = client.list_all_sessions()
62
+ ```
63
+
64
+ `watch_*` returns a plain iterator with the same reconnect-replay semantics as the async side.
65
+
53
66
  ## Scope
54
67
 
55
68
  The server exposes a strictly read-only surface: accounts, sessions, messages, contacts, group members, SNS timelines, media export, and an SSE push stream. This client covers **all of it except the SNS timelines** — the server has those endpoints, the generated layer does not wrap them yet, so reach for plain HTTP if you need them (the gap is deliberate, not silent: the contract pins the surface this client is tested against). See the server repository for the API reference.
@@ -28,6 +28,19 @@ async def main() -> None:
28
28
  asyncio.run(main())
29
29
  ```
30
30
 
31
+ Need synchronous code? `SyncClient` mirrors `Client` method-for-method and drives its own
32
+ private event-loop thread — it neither hijacks nor requires a loop in your thread:
33
+
34
+ ```python
35
+ from weflow_sdk import SyncClient
36
+
37
+ with SyncClient(base_url="http://127.0.0.1:5033", token="your-token") as client:
38
+ client.health()
39
+ sessions = client.list_all_sessions()
40
+ ```
41
+
42
+ `watch_*` returns a plain iterator with the same reconnect-replay semantics as the async side.
43
+
31
44
  ## Scope
32
45
 
33
46
  The server exposes a strictly read-only surface: accounts, sessions, messages, contacts, group members, SNS timelines, media export, and an SSE push stream. This client covers **all of it except the SNS timelines** — the server has those endpoints, the generated layer does not wrap them yet, so reach for plain HTTP if you need them (the gap is deliberate, not silent: the contract pins the surface this client is tested against). See the server repository for the API reference.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "weflow-sdk"
7
- version = "0.9.0"
7
+ version = "0.9.2"
8
8
  description = "Typed Python client for weflow-server's HTTP API (generated models + handwritten behavior layer)"
9
9
  license = "MIT"
10
10
  license-files = ["LICENSE"]
@@ -20,6 +20,7 @@ from .client import (
20
20
  NotReady,
21
21
  ShapeError,
22
22
  StatusError,
23
+ SyncClient,
23
24
  TransportError,
24
25
  )
25
26
 
@@ -33,5 +34,6 @@ __all__ = [
33
34
  "NotReady",
34
35
  "ShapeError",
35
36
  "StatusError",
37
+ "SyncClient",
36
38
  "TransportError",
37
39
  ]
@@ -18,10 +18,11 @@ from __future__ import annotations
18
18
  import asyncio
19
19
  import json as _json
20
20
  import logging
21
+ import threading
21
22
  import urllib.parse
22
- from collections.abc import AsyncIterator, Callable
23
+ from collections.abc import AsyncIterator, Callable, Coroutine, Iterator
23
24
  from dataclasses import dataclass
24
- from typing import Any
25
+ from typing import Any, TypeVar
25
26
 
26
27
  import httpx
27
28
  from pydantic import ValidationError
@@ -374,7 +375,14 @@ class Client:
374
375
  return
375
376
  if mine.state is gen.AccountStatus.ERROR:
376
377
  raise NotReady(timeout, f"error: {mine.error or ''}")
377
- last_state = str(mine.state.value)
378
+ if mine.state is gen.AccountStatus.INDEXING and mine.progress is not None:
379
+ p = mine.progress
380
+ last_state = (
381
+ f"indexing ({p.tables_done} / {p.tables_total} tables, "
382
+ f"{p.messages_indexed} messages indexed)"
383
+ )
384
+ else:
385
+ last_state = str(mine.state.value)
378
386
  elif listing is not None:
379
387
  last_state = "not-registered"
380
388
  if asyncio.get_running_loop().time() >= deadline:
@@ -425,7 +433,7 @@ class Client:
425
433
  The failure reason (``error``) and the message count live only here;
426
434
  ``/health`` collapses everything to a scalar phase.
427
435
  """
428
- listing = self._validate(gen.AccountsList,
436
+ listing = self._validate(gen.AccountsList,
429
437
  await self._get_json("/api/v1/accounts", {})
430
438
  )
431
439
  return listing.accounts
@@ -504,7 +512,7 @@ class Client:
504
512
  query["offset"] = str(offset)
505
513
  if limit is not None:
506
514
  query["limit"] = str(limit)
507
- return self._validate(gen.PullEnvelope,
515
+ return self._validate(gen.PullEnvelope,
508
516
  await self._get_json(
509
517
  f"/api/v1/sessions/{_encode_path_segment(talker)}/messages", query
510
518
  )
@@ -565,7 +573,7 @@ class Client:
565
573
  query["limit"] = str(page_size)
566
574
  if keyword is not None:
567
575
  query["keyword"] = keyword
568
- page = self._validate(gen.SessionsNative,
576
+ page = self._validate(gen.SessionsNative,
569
577
  await self._get_json("/api/v1/sessions", query)
570
578
  )
571
579
  count = len(page.sessions)
@@ -657,7 +665,7 @@ class Client:
657
665
  query = _message_query_params(
658
666
  talker, keyword, start, end, limit, offset, media
659
667
  )
660
- return self._validate(gen.MessagesNative,
668
+ return self._validate(gen.MessagesNative,
661
669
  await self._get_json("/api/v1/messages", query)
662
670
  )
663
671
 
@@ -688,7 +696,7 @@ class Client:
688
696
  query = _message_query_params(
689
697
  talker, keyword, start, end, limit, offset, media
690
698
  )
691
- return self._validate(gen.ChatlabMessages,
699
+ return self._validate(gen.ChatlabMessages,
692
700
  await self._get_json("/chatlab/messages", query)
693
701
  )
694
702
 
@@ -711,7 +719,7 @@ class Client:
711
719
  query["offset"] = str(offset)
712
720
  if keyword is not None:
713
721
  query["keyword"] = keyword
714
- return self._validate(gen.Contacts,
722
+ return self._validate(gen.Contacts,
715
723
  await self._get_json("/api/v1/contacts", query)
716
724
  )
717
725
 
@@ -742,7 +750,7 @@ class Client:
742
750
  query["includeMessageCounts"] = "1"
743
751
  # Counting messages scans every session behind each roster entry: that is
744
752
  # exactly the request the published read budget must not apply to.
745
- return self._validate(gen.GroupMembers,
753
+ return self._validate(gen.GroupMembers,
746
754
  await self._get_json(
747
755
  "/api/v1/group-members", query, long=include_message_counts
748
756
  )
@@ -947,3 +955,254 @@ class Client:
947
955
  # server event kind degrades instead of killing the stream.
948
956
  log.debug("ignoring unknown SSE event kind: %s", kind)
949
957
  return None
958
+
959
+
960
+ _T = TypeVar("_T")
961
+
962
+
963
+ class SyncClient:
964
+ """Synchronous facade over :class:`Client` with its own event-loop thread.
965
+
966
+ Why a dedicated thread: a sync caller may already be inside a running
967
+ asyncio loop (Jupyter, an async web handler, a test harness). Calling
968
+ ``asyncio.run`` there fails with "cannot be called from a running event
969
+ loop"; creating a loop in the calling thread and closing it later also
970
+ conflicts with that thread's loop. Here every coroutine runs on one
971
+ private loop owned by this object, so the caller's loop is untouched.
972
+
973
+ The method surface mirrors :class:`Client` one-to-one; ``watch`` returns a
974
+ normal iterator that keeps the same reconnect/``Last-Event-ID`` semantics.
975
+ """
976
+
977
+ def __init__(
978
+ self,
979
+ base_url: str,
980
+ token: str,
981
+ timeout: float = READ_TIMEOUT,
982
+ stall_timeout: float = STALL_TIMEOUT,
983
+ ) -> None:
984
+ self._closed = False
985
+ self._loop = asyncio.new_event_loop()
986
+ self._ready = threading.Event()
987
+ self._thread = threading.Thread(
988
+ target=self._run_loop, name="weflow-sync-client", daemon=True
989
+ )
990
+ self._thread.start()
991
+ if not self._ready.wait(timeout=5.0):
992
+ self._stop_loop()
993
+ raise ClientError("SyncClient event loop did not start")
994
+ self._client: Client
995
+ try:
996
+ self._client = self._call(
997
+ self._new_client(base_url, token, timeout, stall_timeout)
998
+ )
999
+ except BaseException:
1000
+ self._stop_loop()
1001
+ raise
1002
+
1003
+ # ---- lifecycle ------------------------------------------------------
1004
+
1005
+ def _run_loop(self) -> None:
1006
+ asyncio.set_event_loop(self._loop)
1007
+ # Set ready from *inside* the loop, so __init__ cannot observe a
1008
+ # running thread whose loop has not started scheduling yet.
1009
+ self._loop.call_soon(self._ready.set)
1010
+ try:
1011
+ self._loop.run_forever()
1012
+ finally:
1013
+ self._loop.close()
1014
+
1015
+ async def _new_client(
1016
+ self, base_url: str, token: str, timeout: float, stall_timeout: float
1017
+ ) -> Client:
1018
+ # Construct inside the loop thread: httpx binds its async transport to
1019
+ # the loop that first uses it.
1020
+ return Client(base_url, token, timeout, stall_timeout)
1021
+
1022
+ def _call(self, coro: Coroutine[Any, Any, _T]) -> _T:
1023
+ if self._closed:
1024
+ raise ClientError("SyncClient is closed")
1025
+ if not self._thread.is_alive() or not self._loop.is_running():
1026
+ raise ClientError("SyncClient event loop thread is not running")
1027
+ return asyncio.run_coroutine_threadsafe(coro, self._loop).result()
1028
+
1029
+ def _stop_loop(self) -> None:
1030
+ if self._closed:
1031
+ return
1032
+ self._closed = True
1033
+ if self._thread.is_alive():
1034
+ try:
1035
+ self._loop.call_soon_threadsafe(self._loop.stop)
1036
+ except RuntimeError:
1037
+ pass
1038
+ self._thread.join(timeout=5.0)
1039
+
1040
+ def close(self) -> None:
1041
+ """Close the HTTP client and stop the loop thread (idempotent)."""
1042
+ if self._closed:
1043
+ return
1044
+ try:
1045
+ self._call(self._client.aclose())
1046
+ finally:
1047
+ self._stop_loop()
1048
+
1049
+ def __enter__(self) -> SyncClient:
1050
+ return self
1051
+
1052
+ def __exit__(self, *exc: Any) -> None:
1053
+ self.close()
1054
+
1055
+ # ---- mirrors of Client ----------------------------------------------
1056
+
1057
+ def wait_ready(self, account: str, timeout: float = 120.0) -> None:
1058
+ self._call(self._client.wait_ready(account, timeout))
1059
+
1060
+ def ensure_ready(self, account: str, body: dict, timeout: float = 120.0) -> None:
1061
+ self._call(self._client.ensure_ready(account, body, timeout))
1062
+
1063
+ def health(self) -> gen.Health:
1064
+ return self._call(self._client.health())
1065
+
1066
+ def accounts(self) -> list[gen.AccountStateView]:
1067
+ return self._call(self._client.accounts())
1068
+
1069
+ def register(self, body: dict) -> RegisterOutcome:
1070
+ return self._call(self._client.register(body))
1071
+
1072
+ def sync_now(self) -> gen.SyncResult:
1073
+ return self._call(self._client.sync_now())
1074
+
1075
+ def pull_page(
1076
+ self,
1077
+ talker: str,
1078
+ since: int | None,
1079
+ offset: int = 0,
1080
+ limit: int | None = None,
1081
+ ) -> gen.PullEnvelope:
1082
+ return self._call(self._client.pull_page(talker, since, offset, limit))
1083
+
1084
+ def drain_session(
1085
+ self,
1086
+ talker: str,
1087
+ since: int | None,
1088
+ on_page: Callable[[list[gen.PullMessage]], None],
1089
+ ) -> int:
1090
+ # Loop in the caller thread so on_page runs where the caller expects
1091
+ # it; the per-page request itself is still submitted to the loop thread.
1092
+ next_since = since
1093
+ next_offset = 0
1094
+ total = 0
1095
+ while True:
1096
+ page = self.pull_page(talker, next_since, next_offset)
1097
+ total += len(page.messages)
1098
+ on_page(page.messages)
1099
+ if not page.sync.has_more:
1100
+ return total
1101
+ next_since = page.sync.next_since
1102
+ next_offset = page.sync.next_offset
1103
+
1104
+ def list_all_sessions(
1105
+ self,
1106
+ *,
1107
+ page_size: int | None = None,
1108
+ keyword: str | None = None,
1109
+ ) -> list[gen.SessionNative]:
1110
+ return self._call(self._client.list_all_sessions(page_size=page_size, keyword=keyword))
1111
+
1112
+ def media_bytes(self, message: gen.ChatlabMessage, talker: str) -> bytes:
1113
+ return self._call(self._client.media_bytes(message, talker))
1114
+
1115
+ def list_messages(
1116
+ self,
1117
+ talker: str,
1118
+ *,
1119
+ keyword: str | None = None,
1120
+ start: str | None = None,
1121
+ end: str | None = None,
1122
+ limit: int | None = None,
1123
+ offset: int | None = None,
1124
+ media: bool = False,
1125
+ ) -> gen.MessagesNative:
1126
+ return self._call(
1127
+ self._client.list_messages(
1128
+ talker,
1129
+ keyword=keyword,
1130
+ start=start,
1131
+ end=end,
1132
+ limit=limit,
1133
+ offset=offset,
1134
+ media=media,
1135
+ )
1136
+ )
1137
+
1138
+ def chatlab_messages(
1139
+ self,
1140
+ talker: str,
1141
+ *,
1142
+ keyword: str | None = None,
1143
+ start: str | None = None,
1144
+ end: str | None = None,
1145
+ limit: int | None = None,
1146
+ offset: int | None = None,
1147
+ media: bool = False,
1148
+ ) -> gen.ChatlabMessages:
1149
+ return self._call(
1150
+ self._client.chatlab_messages(
1151
+ talker,
1152
+ keyword=keyword,
1153
+ start=start,
1154
+ end=end,
1155
+ limit=limit,
1156
+ offset=offset,
1157
+ media=media,
1158
+ )
1159
+ )
1160
+
1161
+ def contacts(
1162
+ self,
1163
+ *,
1164
+ limit: int | None = None,
1165
+ offset: int | None = None,
1166
+ keyword: str | None = None,
1167
+ ) -> gen.Contacts:
1168
+ return self._call(self._client.contacts(limit=limit, offset=offset, keyword=keyword))
1169
+
1170
+ def group_members(
1171
+ self,
1172
+ chatroom: str,
1173
+ *,
1174
+ include_message_counts: bool = False,
1175
+ ) -> gen.GroupMembers:
1176
+ return self._call(
1177
+ self._client.group_members(
1178
+ chatroom, include_message_counts=include_message_counts
1179
+ )
1180
+ )
1181
+
1182
+ def media_bytes_by_id(self, media_id: str) -> bytes:
1183
+ return self._call(self._client.media_bytes_by_id(media_id))
1184
+
1185
+ def watch(self) -> Iterator[Any]:
1186
+ """Yield live events, closing the async generator on early exit."""
1187
+ # Annotated as Any only to keep ``aclose`` visible to mypy: the public
1188
+ # Client.watch return type is AsyncIterator, which does not declare it.
1189
+ agen: Any = self._client.watch()
1190
+
1191
+ async def _next() -> Any:
1192
+ return await agen.__anext__()
1193
+
1194
+ async def _aclose() -> None:
1195
+ await agen.aclose()
1196
+
1197
+ try:
1198
+ while True:
1199
+ try:
1200
+ yield self._call(_next())
1201
+ except StopAsyncIteration:
1202
+ return
1203
+ finally:
1204
+ if not self._closed:
1205
+ try:
1206
+ self._call(_aclose())
1207
+ except (StopAsyncIteration, ClientError):
1208
+ pass
@@ -10,6 +10,10 @@
10
10
  "occupied_status": {
11
11
  "$ref": "#/components/schemas/AccountStatus"
12
12
  },
13
+ "progress": {
14
+ "$ref": "#/components/schemas/IndexProgressDto",
15
+ "description": "`indexing` 态才有;其余态整块省略(不是 `null`)。"
16
+ },
13
17
  "state": {
14
18
  "type": "string"
15
19
  },
@@ -116,6 +120,10 @@
116
120
  "db_storage": {
117
121
  "type": "string"
118
122
  },
123
+ "progress": {
124
+ "$ref": "#/components/schemas/IndexProgressDto",
125
+ "description": "`indexing` 态才有;`ready` 态整块省略——完成度是 100%,再报一遍只是冗余键。"
126
+ },
119
127
  "state": {
120
128
  "type": "string"
121
129
  },
@@ -152,6 +160,10 @@
152
160
  "minimum": 0,
153
161
  "type": "integer"
154
162
  },
163
+ "progress": {
164
+ "$ref": "#/components/schemas/IndexProgressDto",
165
+ "description": "`indexing` 态才有;其余态整块省略(条件键,与 `error` 同族)。"
166
+ },
155
167
  "state": {
156
168
  "$ref": "#/components/schemas/AccountStatus"
157
169
  },
@@ -722,6 +734,32 @@
722
734
  ],
723
735
  "type": "object"
724
736
  },
737
+ "IndexProgressDto": {
738
+ "description": "建索引进度(**只在 `indexing` 态出现**)。\n\n三个键同生同灭:客户端要么一次拿到完整的三个数,要么整块缺失——所以每个承载它的\n响应字段都是 `Option` + `skip_serializing_if`(缺省即没有,不是 `null`)。\n\n分母口径是**消息表**:`tables_total` 只数消息库里的 Msg_* 表。session/contact/sns\n是毫秒级,混进分母会让开跑时的百分比虚高再卡住,反而误导「还要等多久」。\n行数逐行推进;读失败的行不计入。表失败时 `tables_done` 会比 `tables_total` 少一格 ——\n那正是「有一条表确实没读成」的诚实表达。",
739
+ "properties": {
740
+ "messages_indexed": {
741
+ "description": "已成功读入索引的消息行累计值(逐行推进;读失败的行不计)。",
742
+ "minimum": 0,
743
+ "type": "integer"
744
+ },
745
+ "tables_done": {
746
+ "description": "已扫完的消息表数。",
747
+ "minimum": 0,
748
+ "type": "integer"
749
+ },
750
+ "tables_total": {
751
+ "description": "消息表总数(本账号本次构建的枚举结果)。",
752
+ "minimum": 0,
753
+ "type": "integer"
754
+ }
755
+ },
756
+ "required": [
757
+ "messages_indexed",
758
+ "tables_done",
759
+ "tables_total"
760
+ ],
761
+ "type": "object"
762
+ },
725
763
  "MediaBrief": {
726
764
  "description": "一条消息的媒体**元数据**(拉取面与消息面同形)。\n\n它**不是**「字节可取」的承诺:无媒体时整个键省略;`fileName` 只有在导出确实写出了本地\n副本之后才是可取句柄(那时它是**实际导出文件名**),否则它只是这条消息自带的文件名。\n`md5` 取不到时省略该键 —— 未导出不等于没有摘要。",
727
765
  "properties": {
@@ -1348,7 +1386,7 @@
1348
1386
  },
1349
1387
  "info": {
1350
1388
  "title": "weflow-server",
1351
- "version": "0.9.0"
1389
+ "version": "0.9.2"
1352
1390
  },
1353
1391
  "openapi": "3.0.3",
1354
1392
  "paths": {
@@ -7,7 +7,7 @@
7
7
 
8
8
  No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
9
9
 
10
- The version of the OpenAPI document: 0.9.0
10
+ The version of the OpenAPI document: 0.9.2
11
11
  Generated by OpenAPI Generator (https://openapi-generator.tech)
12
12
 
13
13
  Do not edit the class manually.
@@ -52,6 +52,7 @@ __all__ = [
52
52
  "GroupMember",
53
53
  "GroupMembers",
54
54
  "Health",
55
+ "IndexProgressDto",
55
56
  "MediaBrief",
56
57
  "MediaEnvelope",
57
58
  "MediaObject",
@@ -113,6 +114,7 @@ from weflow_sdk.generated.weflow_sdk.models.event_sync import EventSync as Event
113
114
  from weflow_sdk.generated.weflow_sdk.models.group_member import GroupMember as GroupMember
114
115
  from weflow_sdk.generated.weflow_sdk.models.group_members import GroupMembers as GroupMembers
115
116
  from weflow_sdk.generated.weflow_sdk.models.health import Health as Health
117
+ from weflow_sdk.generated.weflow_sdk.models.index_progress_dto import IndexProgressDto as IndexProgressDto
116
118
  from weflow_sdk.generated.weflow_sdk.models.media_brief import MediaBrief as MediaBrief
117
119
  from weflow_sdk.generated.weflow_sdk.models.media_envelope import MediaEnvelope as MediaEnvelope
118
120
  from weflow_sdk.generated.weflow_sdk.models.media_object import MediaObject as MediaObject
@@ -3,7 +3,7 @@
3
3
 
4
4
  No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
5
5
 
6
- The version of the OpenAPI document: 0.9.0
6
+ The version of the OpenAPI document: 0.9.2
7
7
  Generated by OpenAPI Generator (https://openapi-generator.tech)
8
8
 
9
9
  Do not edit the class manually.
@@ -3,7 +3,7 @@
3
3
 
4
4
  No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
5
5
 
6
- The version of the OpenAPI document: 0.9.0
6
+ The version of the OpenAPI document: 0.9.2
7
7
  Generated by OpenAPI Generator (https://openapi-generator.tech)
8
8
 
9
9
  Do not edit the class manually.
@@ -3,7 +3,7 @@
3
3
 
4
4
  No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
5
5
 
6
- The version of the OpenAPI document: 0.9.0
6
+ The version of the OpenAPI document: 0.9.2
7
7
  Generated by OpenAPI Generator (https://openapi-generator.tech)
8
8
 
9
9
  Do not edit the class manually.
@@ -522,7 +522,7 @@ class Configuration:
522
522
  return "Python SDK Debug Report:\n"\
523
523
  "OS: {env}\n"\
524
524
  "Python Version: {pyversion}\n"\
525
- "Version of the API: 0.9.0\n"\
525
+ "Version of the API: 0.9.2\n"\
526
526
  "SDK Package Version: 1.0.0".\
527
527
  format(env=sys.platform, pyversion=sys.version)
528
528
 
@@ -3,7 +3,7 @@
3
3
 
4
4
  No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
5
5
 
6
- The version of the OpenAPI document: 0.9.0
6
+ The version of the OpenAPI document: 0.9.2
7
7
  Generated by OpenAPI Generator (https://openapi-generator.tech)
8
8
 
9
9
  Do not edit the class manually.
@@ -6,7 +6,7 @@
6
6
 
7
7
  No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
8
8
 
9
- The version of the OpenAPI document: 0.9.0
9
+ The version of the OpenAPI document: 0.9.2
10
10
  Generated by OpenAPI Generator (https://openapi-generator.tech)
11
11
 
12
12
  Do not edit the class manually.
@@ -37,6 +37,7 @@ from weflow_sdk.generated.weflow_sdk.models.event_sync import EventSync
37
37
  from weflow_sdk.generated.weflow_sdk.models.group_member import GroupMember
38
38
  from weflow_sdk.generated.weflow_sdk.models.group_members import GroupMembers
39
39
  from weflow_sdk.generated.weflow_sdk.models.health import Health
40
+ from weflow_sdk.generated.weflow_sdk.models.index_progress_dto import IndexProgressDto
40
41
  from weflow_sdk.generated.weflow_sdk.models.media_brief import MediaBrief
41
42
  from weflow_sdk.generated.weflow_sdk.models.media_envelope import MediaEnvelope
42
43
  from weflow_sdk.generated.weflow_sdk.models.media_object import MediaObject
@@ -5,7 +5,7 @@
5
5
 
6
6
  No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
7
7
 
8
- The version of the OpenAPI document: 0.9.0
8
+ The version of the OpenAPI document: 0.9.2
9
9
  Generated by OpenAPI Generator (https://openapi-generator.tech)
10
10
 
11
11
  Do not edit the class manually.
@@ -18,8 +18,9 @@ import re # noqa: F401
18
18
  import json
19
19
 
20
20
  from pydantic import BaseModel, ConfigDict, StrictBool, StrictStr
21
- from typing import Any, ClassVar, Dict, List
21
+ from typing import Any, ClassVar, Dict, List, Optional
22
22
  from weflow_sdk.generated.weflow_sdk.models.account_status import AccountStatus
23
+ from weflow_sdk.generated.weflow_sdk.models.index_progress_dto import IndexProgressDto
23
24
  from typing import Optional, Set
24
25
  from typing_extensions import Self
25
26
  from pydantic_core import to_jsonable_python
@@ -30,10 +31,11 @@ class AccountConflict(BaseModel):
30
31
  """ # noqa: E501
31
32
  occupied_by: StrictStr
32
33
  occupied_status: AccountStatus
34
+ progress: Optional[IndexProgressDto] = None
33
35
  state: StrictStr
34
36
  success: StrictBool
35
37
  wxid: StrictStr
36
- __properties: ClassVar[List[str]] = ["occupied_by", "occupied_status", "state", "success", "wxid"]
38
+ __properties: ClassVar[List[str]] = ["occupied_by", "occupied_status", "progress", "state", "success", "wxid"]
37
39
 
38
40
  model_config = ConfigDict(
39
41
  validate_by_name=True,
@@ -74,6 +76,9 @@ class AccountConflict(BaseModel):
74
76
  exclude=excluded_fields,
75
77
  exclude_none=True,
76
78
  )
79
+ # override the default output from pydantic by calling `to_dict()` of progress
80
+ if self.progress:
81
+ _dict['progress'] = self.progress.to_dict()
77
82
  return _dict
78
83
 
79
84
  @classmethod
@@ -88,6 +93,7 @@ class AccountConflict(BaseModel):
88
93
  _obj = cls.model_validate({
89
94
  "occupied_by": obj.get("occupied_by"),
90
95
  "occupied_status": obj.get("occupied_status"),
96
+ "progress": IndexProgressDto.from_dict(obj["progress"]) if obj.get("progress") is not None else None,
91
97
  "state": obj.get("state"),
92
98
  "success": obj.get("success"),
93
99
  "wxid": obj.get("wxid")
@@ -5,7 +5,7 @@
5
5
 
6
6
  No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
7
7
 
8
- The version of the OpenAPI document: 0.9.0
8
+ The version of the OpenAPI document: 0.9.2
9
9
  Generated by OpenAPI Generator (https://openapi-generator.tech)
10
10
 
11
11
  Do not edit the class manually.
@@ -5,7 +5,7 @@
5
5
 
6
6
  No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
7
7
 
8
- The version of the OpenAPI document: 0.9.0
8
+ The version of the OpenAPI document: 0.9.2
9
9
  Generated by OpenAPI Generator (https://openapi-generator.tech)
10
10
 
11
11
  Do not edit the class manually.
@@ -5,7 +5,7 @@
5
5
 
6
6
  No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
7
7
 
8
- The version of the OpenAPI document: 0.9.0
8
+ The version of the OpenAPI document: 0.9.2
9
9
  Generated by OpenAPI Generator (https://openapi-generator.tech)
10
10
 
11
11
  Do not edit the class manually.