servo-client 0.2.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 (166) hide show
  1. servo_client-0.2.0/MANIFEST.in +5 -0
  2. servo_client-0.2.0/PKG-INFO +146 -0
  3. servo_client-0.2.0/README.md +129 -0
  4. servo_client-0.2.0/pyproject.toml +56 -0
  5. servo_client-0.2.0/servo_client_build_backend.py +356 -0
  6. servo_client-0.2.0/setup.cfg +4 -0
  7. servo_client-0.2.0/src/servo/__init__.py +119 -0
  8. servo_client-0.2.0/src/servo/agent/__init__.py +13 -0
  9. servo_client-0.2.0/src/servo/agent/__main__.py +6 -0
  10. servo_client-0.2.0/src/servo/agent/cli.py +55 -0
  11. servo_client-0.2.0/src/servo/agent/daemon.py +1225 -0
  12. servo_client-0.2.0/src/servo/agent/protocol.py +279 -0
  13. servo_client-0.2.0/src/servo/agent/shim.py +356 -0
  14. servo_client-0.2.0/src/servo/cli.py +354 -0
  15. servo_client-0.2.0/src/servo/client.py +322 -0
  16. servo_client-0.2.0/src/servo/constants.py +88 -0
  17. servo_client-0.2.0/src/servo/direct.py +1211 -0
  18. servo_client-0.2.0/src/servo/errors.py +111 -0
  19. servo_client-0.2.0/src/servo/execution/__init__.py +1 -0
  20. servo_client-0.2.0/src/servo/execution/_gpu_jpeg_encode.py +538 -0
  21. servo_client-0.2.0/src/servo/execution/act_recording.py +582 -0
  22. servo_client-0.2.0/src/servo/execution/action_session_native_quic.py +951 -0
  23. servo_client-0.2.0/src/servo/execution/action_session_options.py +33 -0
  24. servo_client-0.2.0/src/servo/execution/action_session_quic.py +433 -0
  25. servo_client-0.2.0/src/servo/execution/action_session_transport/__init__.py +1946 -0
  26. servo_client-0.2.0/src/servo/execution/action_session_transport/_channel_types.py +99 -0
  27. servo_client-0.2.0/src/servo/execution/action_session_transport/_channels.py +1025 -0
  28. servo_client-0.2.0/src/servo/execution/action_session_transport/_control_verbs.py +321 -0
  29. servo_client-0.2.0/src/servo/execution/action_session_transport/_livekit_channel.py +487 -0
  30. servo_client-0.2.0/src/servo/execution/action_session_transport/_recording.py +151 -0
  31. servo_client-0.2.0/src/servo/execution/action_session_transport/_recovery.py +73 -0
  32. servo_client-0.2.0/src/servo/execution/action_session_transport/_resumption.py +223 -0
  33. servo_client-0.2.0/src/servo/execution/adaptive_chunking.py +183 -0
  34. servo_client-0.2.0/src/servo/execution/cadence.py +950 -0
  35. servo_client-0.2.0/src/servo/execution/chunk_scheduler.py +885 -0
  36. servo_client-0.2.0/src/servo/execution/chunked_loop.py +1041 -0
  37. servo_client-0.2.0/src/servo/execution/chunked_transport.py +154 -0
  38. servo_client-0.2.0/src/servo/execution/encoded_observation.py +1274 -0
  39. servo_client-0.2.0/src/servo/execution/gripper_channels.py +75 -0
  40. servo_client-0.2.0/src/servo/execution/h264_observation.py +853 -0
  41. servo_client-0.2.0/src/servo/execution/multistep_solvers.py +43 -0
  42. servo_client-0.2.0/src/servo/execution/nat_punch.py +326 -0
  43. servo_client-0.2.0/src/servo/execution/observation_encoders.py +748 -0
  44. servo_client-0.2.0/src/servo/execution/policy.py +298 -0
  45. servo_client-0.2.0/src/servo/execution/robot_agent.py +51 -0
  46. servo_client-0.2.0/src/servo/execution/robot_agent_protocol.py +86 -0
  47. servo_client-0.2.0/src/servo/execution/session.py +460 -0
  48. servo_client-0.2.0/src/servo/execution/sfa_observation.py +244 -0
  49. servo_client-0.2.0/src/servo/execution/speculative_action_decode.py +201 -0
  50. servo_client-0.2.0/src/servo/execution/split_expert.py +433 -0
  51. servo_client-0.2.0/src/servo/execution/trajectory_blender.py +479 -0
  52. servo_client-0.2.0/src/servo/execution/world.py +76 -0
  53. servo_client-0.2.0/src/servo/hardware.py +1287 -0
  54. servo_client-0.2.0/src/servo/namespaces/__init__.py +1 -0
  55. servo_client-0.2.0/src/servo/namespaces/adapters.py +26 -0
  56. servo_client-0.2.0/src/servo/namespaces/datasets.py +214 -0
  57. servo_client-0.2.0/src/servo/namespaces/deployments.py +22 -0
  58. servo_client-0.2.0/src/servo/namespaces/jobs.py +109 -0
  59. servo_client-0.2.0/src/servo/namespaces/licenses.py +23 -0
  60. servo_client-0.2.0/src/servo/namespaces/models.py +464 -0
  61. servo_client-0.2.0/src/servo/namespaces/robots.py +104 -0
  62. servo_client-0.2.0/src/servo/namespaces/secrets.py +43 -0
  63. servo_client-0.2.0/src/servo/namespaces/self_hosted.py +55 -0
  64. servo_client-0.2.0/src/servo/public_api.py +139 -0
  65. servo_client-0.2.0/src/servo/resources/__init__.py +1 -0
  66. servo_client-0.2.0/src/servo/resources/adapter.py +79 -0
  67. servo_client-0.2.0/src/servo/resources/base.py +159 -0
  68. servo_client-0.2.0/src/servo/resources/dataset.py +514 -0
  69. servo_client-0.2.0/src/servo/resources/deployment.py +329 -0
  70. servo_client-0.2.0/src/servo/resources/embodiment.py +41 -0
  71. servo_client-0.2.0/src/servo/resources/generation.py +110 -0
  72. servo_client-0.2.0/src/servo/resources/hf_import.py +317 -0
  73. servo_client-0.2.0/src/servo/resources/job.py +224 -0
  74. servo_client-0.2.0/src/servo/resources/model.py +383 -0
  75. servo_client-0.2.0/src/servo/resources/secret.py +38 -0
  76. servo_client-0.2.0/src/servo/resources/self_hosted.py +133 -0
  77. servo_client-0.2.0/src/servo/resources/upload.py +35 -0
  78. servo_client-0.2.0/src/servo/resources/usage.py +100 -0
  79. servo_client-0.2.0/src/servo/templates.py +51 -0
  80. servo_client-0.2.0/src/servo/types.py +161 -0
  81. servo_client-0.2.0/src/servo_action_session/__init__.py +24 -0
  82. servo_client-0.2.0/src/servo_action_session/effects.py +33 -0
  83. servo_client-0.2.0/src/servo_action_session/events.py +53 -0
  84. servo_client-0.2.0/src/servo_action_session/reducer.py +783 -0
  85. servo_client-0.2.0/src/servo_action_session/risk_switching.py +794 -0
  86. servo_client-0.2.0/src/servo_action_session/state.py +168 -0
  87. servo_client-0.2.0/src/servo_client.egg-info/PKG-INFO +146 -0
  88. servo_client-0.2.0/src/servo_client.egg-info/SOURCES.txt +164 -0
  89. servo_client-0.2.0/src/servo_client.egg-info/dependency_links.txt +1 -0
  90. servo_client-0.2.0/src/servo_client.egg-info/entry_points.txt +2 -0
  91. servo_client-0.2.0/src/servo_client.egg-info/requires.txt +10 -0
  92. servo_client-0.2.0/src/servo_client.egg-info/top_level.txt +6 -0
  93. servo_client-0.2.0/src/servo_contracts/__init__.py +189 -0
  94. servo_client-0.2.0/src/servo_contracts/_compat.py +53 -0
  95. servo_client-0.2.0/src/servo_contracts/action_policy.py +102 -0
  96. servo_client-0.2.0/src/servo_contracts/action_session.py +2001 -0
  97. servo_client-0.2.0/src/servo_contracts/action_session_lifecycle.py +451 -0
  98. servo_client-0.2.0/src/servo_contracts/action_session_quic.py +190 -0
  99. servo_client-0.2.0/src/servo_contracts/action_session_v4.py +1805 -0
  100. servo_client-0.2.0/src/servo_contracts/action_session_v4_pb2.py +129 -0
  101. servo_client-0.2.0/src/servo_contracts/action_session_wire.py +140 -0
  102. servo_client-0.2.0/src/servo_contracts/action_stream.py +147 -0
  103. servo_client-0.2.0/src/servo_contracts/artifacts.py +62 -0
  104. servo_client-0.2.0/src/servo_contracts/benchmark.py +258 -0
  105. servo_client-0.2.0/src/servo_contracts/compiled_execution.py +915 -0
  106. servo_client-0.2.0/src/servo_contracts/configuration.py +91 -0
  107. servo_client-0.2.0/src/servo_contracts/content_identity.py +55 -0
  108. servo_client-0.2.0/src/servo_contracts/environment.py +253 -0
  109. servo_client-0.2.0/src/servo_contracts/errors.py +296 -0
  110. servo_client-0.2.0/src/servo_contracts/execution_evidence.py +117 -0
  111. servo_client-0.2.0/src/servo_contracts/git_identity.py +140 -0
  112. servo_client-0.2.0/src/servo_contracts/hf_tasks.py +169 -0
  113. servo_client-0.2.0/src/servo_contracts/inference.py +207 -0
  114. servo_client-0.2.0/src/servo_contracts/observability.py +365 -0
  115. servo_client-0.2.0/src/servo_contracts/observation.py +330 -0
  116. servo_client-0.2.0/src/servo_contracts/onboarding.py +656 -0
  117. servo_client-0.2.0/src/servo_contracts/openpi.py +120 -0
  118. servo_client-0.2.0/src/servo_contracts/optimization/__init__.py +108 -0
  119. servo_client-0.2.0/src/servo_contracts/optimization/base.py +59 -0
  120. servo_client-0.2.0/src/servo_contracts/optimization/comparison.py +689 -0
  121. servo_client-0.2.0/src/servo_contracts/optimization/enums.py +130 -0
  122. servo_client-0.2.0/src/servo_contracts/optimization/execution.py +740 -0
  123. servo_client-0.2.0/src/servo_contracts/optimization/hardware.py +257 -0
  124. servo_client-0.2.0/src/servo_contracts/optimization/search.py +380 -0
  125. servo_client-0.2.0/src/servo_contracts/qualification.py +48 -0
  126. servo_client-0.2.0/src/servo_contracts/release.py +188 -0
  127. servo_client-0.2.0/src/servo_contracts/repository.py +138 -0
  128. servo_client-0.2.0/src/servo_contracts/resources.py +155 -0
  129. servo_client-0.2.0/src/servo_contracts/risk_switching.py +425 -0
  130. servo_client-0.2.0/src/servo_contracts/robot_agent_v1_pb2.py +55 -0
  131. servo_client-0.2.0/src/servo_contracts/robot_control.py +187 -0
  132. servo_client-0.2.0/src/servo_contracts/runtime.py +350 -0
  133. servo_client-0.2.0/src/servo_contracts/self_hosted.py +329 -0
  134. servo_client-0.2.0/src/servo_contracts/serving.py +566 -0
  135. servo_client-0.2.0/src/servo_contracts/solvers.py +569 -0
  136. servo_client-0.2.0/src/servo_contracts/templates.py +273 -0
  137. servo_client-0.2.0/src/servo_data/__init__.py +5 -0
  138. servo_client-0.2.0/src/servo_data/streaming.py +735 -0
  139. servo_client-0.2.0/src/servo_observation/__init__.py +86 -0
  140. servo_client-0.2.0/src/servo_observation/clocks.py +47 -0
  141. servo_client-0.2.0/src/servo_observation/errors.py +103 -0
  142. servo_client-0.2.0/src/servo_observation/frames.py +386 -0
  143. servo_client-0.2.0/src/servo_observation/identity.py +126 -0
  144. servo_client-0.2.0/src/servo_observation/pipeline.py +366 -0
  145. servo_client-0.2.0/src/servo_observation/routes.py +207 -0
  146. servo_client-0.2.0/src/servo_telemetry/__init__.py +48 -0
  147. servo_client-0.2.0/src/servo_telemetry/api.py +285 -0
  148. servo_client-0.2.0/src/servo_telemetry/bootstrap.py +95 -0
  149. servo_client-0.2.0/src/servo_telemetry/boundaries.py +219 -0
  150. servo_client-0.2.0/src/servo_telemetry/catalog.py +664 -0
  151. servo_client-0.2.0/src/servo_telemetry/config.py +161 -0
  152. servo_client-0.2.0/src/servo_telemetry/context.py +60 -0
  153. servo_client-0.2.0/src/servo_telemetry/contracts.py +671 -0
  154. servo_client-0.2.0/src/servo_telemetry/declarations.py +229 -0
  155. servo_client-0.2.0/src/servo_telemetry/discovery.py +133 -0
  156. servo_client-0.2.0/src/servo_telemetry/dispatch.py +220 -0
  157. servo_client-0.2.0/src/servo_telemetry/model.py +166 -0
  158. servo_client-0.2.0/src/servo_telemetry/nvtx.py +183 -0
  159. servo_client-0.2.0/src/servo_telemetry/otlp_http.py +31 -0
  160. servo_client-0.2.0/src/servo_telemetry/profiling.py +82 -0
  161. servo_client-0.2.0/src/servo_telemetry/resource.py +321 -0
  162. servo_client-0.2.0/src/servo_telemetry/sinks/__init__.py +1 -0
  163. servo_client-0.2.0/src/servo_telemetry/sinks/capture.py +33 -0
  164. servo_client-0.2.0/src/servo_telemetry/sinks/metrics.py +197 -0
  165. servo_client-0.2.0/src/servo_telemetry/sinks/noop.py +17 -0
  166. servo_client-0.2.0/src/servo_telemetry/sinks/otel.py +233 -0
@@ -0,0 +1,5 @@
1
+ include README.md
2
+ include servo_client_build_backend.py
3
+ graft src
4
+ global-exclude __pycache__
5
+ global-exclude *.py[cod]
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: servo-client
3
+ Version: 0.2.0
4
+ Summary: Python SDK for Servo's hosted robot model API
5
+ Project-URL: Documentation, https://servo.mintlify.site/
6
+ Requires-Python: >=3.12
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: cryptography>=42
9
+ Requires-Dist: httpx[http2]
10
+ Requires-Dist: msgpack>=1.0.5
11
+ Requires-Dist: numpy>=1.26
12
+ Requires-Dist: pydantic>=2
13
+ Requires-Dist: protobuf<7,>=6.33.6
14
+ Requires-Dist: websockets>=13
15
+ Provides-Extra: livekit
16
+ Requires-Dist: livekit==1.1.14; extra == "livekit"
17
+
18
+ # servo-client
19
+
20
+ The public Python SDK and customer CLI for Servo robot policies, world models, datasets, jobs, and
21
+ deployments.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install 'servo-client>=0.2,<0.3'
27
+ ```
28
+
29
+ Servo releases two architecture-specific Linux wheels containing the native Quinn action-session
30
+ binary, one portable wheel, and a source distribution. Native clients prefer a validated QUIC
31
+ route; WSS and H2 binary remain ordered pre-inference fallbacks. Portable installations use the
32
+ advertised non-native routes. Every route carries the same ActionSession v4 Protobuf frames.
33
+
34
+ LiveKit is an explicit optional transport:
35
+
36
+ ```bash
37
+ pip install 'servo-client[livekit]>=0.2,<0.3'
38
+ ```
39
+
40
+ ## Configure the control plane
41
+
42
+ Servo has no compiled-in hosted URL. Set the origin supplied by your Servo administrator so an old
43
+ package cannot silently send credentials or robot traffic to a retired deployment:
44
+
45
+ ```bash
46
+ export SERVO_BASE_URL='https://<your-servo-control-plane>'
47
+ ```
48
+
49
+ The client also accepts `base_url=` explicitly. The argument takes precedence over the environment.
50
+
51
+ ## Sign in
52
+
53
+ The distribution installs the customer-safe `servo` CLI:
54
+
55
+ ```bash
56
+ servo login
57
+ servo whoami
58
+ servo key create --label laptop
59
+ servo deployment list
60
+ ```
61
+
62
+ Serving, benchmarking, profiling, and platform-operator commands belong to the private
63
+ `servo-platform` distribution and are not installed by `servo-client`.
64
+
65
+ ## Quickstart
66
+
67
+ ```python
68
+ import os
69
+
70
+ import servo
71
+ from servo import templates
72
+
73
+ sv = servo.Servo(
74
+ base_url=os.environ["SERVO_BASE_URL"],
75
+ api_key=os.environ["SERVO_API_KEY"],
76
+ )
77
+
78
+ robot = sv.robots.register(templates.so101())
79
+ observation = templates.sample_observation(robot)
80
+ model = sv.models.get("smolvla")
81
+
82
+ report = model.inspect(robot)
83
+ if not report.compatible:
84
+ raise RuntimeError(report.issues)
85
+
86
+ prediction = model.policy(robot, instruction="pick up the cube").act(observation)
87
+ print(prediction.actions[0])
88
+ ```
89
+
90
+ ## Action sessions
91
+
92
+ `sv.session(policy)` negotiates the transports advertised by the selected deployment. Once an
93
+ inference request could have executed, Servo never replays it over another route.
94
+
95
+ LiveKit requires both deployment and session opt-in:
96
+
97
+ ```python
98
+ provider = servo.ServingProvider(os.environ["SERVO_SERVE_PROVIDER"])
99
+ deployment = model.deploy(
100
+ robot,
101
+ serve_provider=provider,
102
+ action_session_channels=("livekit", "wss"),
103
+ )
104
+ policy = deployment.policy(robot, instruction="pick up the cube")
105
+
106
+ with sv.session(
107
+ policy,
108
+ action_session=servo.ActionSessionOptions(channels=("livekit",)),
109
+ ) as session:
110
+ prediction = session.act(observation)
111
+ ```
112
+
113
+ ## Public boundary
114
+
115
+ Application compatibility is promised for documented imports from `servo` and these client
116
+ namespaces:
117
+
118
+ - Stable: `robots`, `adapters`, `models`, `datasets`/`data`, `deployments`, `jobs`, `licenses`, and
119
+ `secrets`.
120
+ - Advanced: direct endpoint helpers (`servo.connect`, `servo.attach`, `EndpointGrant`) and
121
+ `sv.self_hosted`.
122
+
123
+ The distribution also carries dependency-light support packages:
124
+
125
+ - Extension contracts: `servo_action_session`, `servo_data`, and `servo_observation`.
126
+ - Implementation dependencies: `servo_contracts` and `servo_telemetry`.
127
+
128
+ Importability of an implementation package is not a customer compatibility promise. The client
129
+ wheel excludes product-only profiler integration and does not contain `servo_platform`,
130
+ `servo_runtimes`, FastAPI routes, database models, provider launchers, provider credentials, or
131
+ private endpoint URLs.
132
+
133
+ ## Failure and retry policy
134
+
135
+ The client does not automatically replay mutations or robot actions. Transport and server failures
136
+ are returned as typed Servo errors so applications can choose a recovery policy appropriate to the
137
+ operation. Idempotent reads may be retried by application code. Configure `timeout=` on
138
+ `servo.Servo`; long-running synchronous jobs can require a larger read timeout than catalog calls.
139
+
140
+ ## Requirements
141
+
142
+ - Python 3.12+
143
+ - A Servo control-plane URL
144
+ - An organization API key for authenticated operations
145
+
146
+ Full documentation: <https://servo.mintlify.site/>
@@ -0,0 +1,129 @@
1
+ # servo-client
2
+
3
+ The public Python SDK and customer CLI for Servo robot policies, world models, datasets, jobs, and
4
+ deployments.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pip install 'servo-client>=0.2,<0.3'
10
+ ```
11
+
12
+ Servo releases two architecture-specific Linux wheels containing the native Quinn action-session
13
+ binary, one portable wheel, and a source distribution. Native clients prefer a validated QUIC
14
+ route; WSS and H2 binary remain ordered pre-inference fallbacks. Portable installations use the
15
+ advertised non-native routes. Every route carries the same ActionSession v4 Protobuf frames.
16
+
17
+ LiveKit is an explicit optional transport:
18
+
19
+ ```bash
20
+ pip install 'servo-client[livekit]>=0.2,<0.3'
21
+ ```
22
+
23
+ ## Configure the control plane
24
+
25
+ Servo has no compiled-in hosted URL. Set the origin supplied by your Servo administrator so an old
26
+ package cannot silently send credentials or robot traffic to a retired deployment:
27
+
28
+ ```bash
29
+ export SERVO_BASE_URL='https://<your-servo-control-plane>'
30
+ ```
31
+
32
+ The client also accepts `base_url=` explicitly. The argument takes precedence over the environment.
33
+
34
+ ## Sign in
35
+
36
+ The distribution installs the customer-safe `servo` CLI:
37
+
38
+ ```bash
39
+ servo login
40
+ servo whoami
41
+ servo key create --label laptop
42
+ servo deployment list
43
+ ```
44
+
45
+ Serving, benchmarking, profiling, and platform-operator commands belong to the private
46
+ `servo-platform` distribution and are not installed by `servo-client`.
47
+
48
+ ## Quickstart
49
+
50
+ ```python
51
+ import os
52
+
53
+ import servo
54
+ from servo import templates
55
+
56
+ sv = servo.Servo(
57
+ base_url=os.environ["SERVO_BASE_URL"],
58
+ api_key=os.environ["SERVO_API_KEY"],
59
+ )
60
+
61
+ robot = sv.robots.register(templates.so101())
62
+ observation = templates.sample_observation(robot)
63
+ model = sv.models.get("smolvla")
64
+
65
+ report = model.inspect(robot)
66
+ if not report.compatible:
67
+ raise RuntimeError(report.issues)
68
+
69
+ prediction = model.policy(robot, instruction="pick up the cube").act(observation)
70
+ print(prediction.actions[0])
71
+ ```
72
+
73
+ ## Action sessions
74
+
75
+ `sv.session(policy)` negotiates the transports advertised by the selected deployment. Once an
76
+ inference request could have executed, Servo never replays it over another route.
77
+
78
+ LiveKit requires both deployment and session opt-in:
79
+
80
+ ```python
81
+ provider = servo.ServingProvider(os.environ["SERVO_SERVE_PROVIDER"])
82
+ deployment = model.deploy(
83
+ robot,
84
+ serve_provider=provider,
85
+ action_session_channels=("livekit", "wss"),
86
+ )
87
+ policy = deployment.policy(robot, instruction="pick up the cube")
88
+
89
+ with sv.session(
90
+ policy,
91
+ action_session=servo.ActionSessionOptions(channels=("livekit",)),
92
+ ) as session:
93
+ prediction = session.act(observation)
94
+ ```
95
+
96
+ ## Public boundary
97
+
98
+ Application compatibility is promised for documented imports from `servo` and these client
99
+ namespaces:
100
+
101
+ - Stable: `robots`, `adapters`, `models`, `datasets`/`data`, `deployments`, `jobs`, `licenses`, and
102
+ `secrets`.
103
+ - Advanced: direct endpoint helpers (`servo.connect`, `servo.attach`, `EndpointGrant`) and
104
+ `sv.self_hosted`.
105
+
106
+ The distribution also carries dependency-light support packages:
107
+
108
+ - Extension contracts: `servo_action_session`, `servo_data`, and `servo_observation`.
109
+ - Implementation dependencies: `servo_contracts` and `servo_telemetry`.
110
+
111
+ Importability of an implementation package is not a customer compatibility promise. The client
112
+ wheel excludes product-only profiler integration and does not contain `servo_platform`,
113
+ `servo_runtimes`, FastAPI routes, database models, provider launchers, provider credentials, or
114
+ private endpoint URLs.
115
+
116
+ ## Failure and retry policy
117
+
118
+ The client does not automatically replay mutations or robot actions. Transport and server failures
119
+ are returned as typed Servo errors so applications can choose a recovery policy appropriate to the
120
+ operation. Idempotent reads may be retried by application code. Configure `timeout=` on
121
+ `servo.Servo`; long-running synchronous jobs can require a larger read timeout than catalog calls.
122
+
123
+ ## Requirements
124
+
125
+ - Python 3.12+
126
+ - A Servo control-plane URL
127
+ - An organization API key for authenticated operations
128
+
129
+ Full documentation: <https://servo.mintlify.site/>
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "servo_client_build_backend"
4
+ backend-path = ["."]
5
+
6
+ [project]
7
+ name = "servo-client"
8
+ version = "0.2.0"
9
+ description = "Python SDK for Servo's hosted robot model API"
10
+ readme = "README.md"
11
+ requires-python = ">=3.12"
12
+ dependencies = [
13
+ "cryptography>=42",
14
+ "httpx[http2]",
15
+ "msgpack>=1.0.5",
16
+ "numpy>=1.26",
17
+ "pydantic>=2",
18
+ "protobuf>=6.33.6,<7",
19
+ "websockets>=13",
20
+ ]
21
+
22
+ [project.optional-dependencies]
23
+ livekit = ["livekit==1.1.14"]
24
+
25
+ [project.scripts]
26
+ servo = "servo.cli:main"
27
+
28
+ [project.urls]
29
+ Documentation = "https://servo.mintlify.site/"
30
+
31
+ [tool.setuptools]
32
+ package-dir = {"" = "src"}
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
36
+ include = [
37
+ "servo",
38
+ "servo.*",
39
+ "servo_action_session",
40
+ "servo_action_session.*",
41
+ "servo_contracts",
42
+ "servo_contracts.*",
43
+ "servo_data",
44
+ "servo_data.*",
45
+ "servo_observation",
46
+ "servo_observation.*",
47
+ "servo_telemetry",
48
+ "servo_telemetry.*",
49
+ ]
50
+ exclude = ["servo_platform*"]
51
+
52
+ # The native QUIC data plane is the default transport, so the compiled artifact
53
+ # ships with the client rather than being something the user has to locate or
54
+ # build. Wheels carrying it are platform-specific by construction.
55
+ [tool.setuptools.package-data]
56
+ servo = ["_native/servo-action-session-quic"]
@@ -0,0 +1,356 @@
1
+ """Build the public client from the monorepo's canonical Python sources.
2
+
3
+ The public distribution is a strict projection of selected packages in the repository-level
4
+ ``src`` tree. A PEP 517 source distribution must be self-contained, so pointing setuptools at
5
+ ``../../src`` produces an sdist whose paths escape the archive and whose wheel cannot be rebuilt.
6
+ This backend stages only the public packages into the conventional local ``src`` layout before
7
+ delegating to setuptools. The generated sdist therefore contains its complete, immutable build
8
+ input and can be built independently of the Servo repository.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import ast
14
+ import os
15
+ import shutil
16
+ import tempfile
17
+ from collections.abc import Iterator, Mapping
18
+ from contextlib import contextmanager
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from setuptools import build_meta as _setuptools
23
+
24
+
25
+ _PROJECT_ROOT = Path(__file__).resolve().parent
26
+
27
+
28
+ def _contract_value(name: str) -> tuple[str, ...]:
29
+ """Read a tuple from the data-only SDK contract without importing ``servo``."""
30
+
31
+ candidates = (
32
+ _PROJECT_ROOT / "src" / "servo" / "public_api.py",
33
+ (_PROJECT_ROOT / ".." / ".." / "src" / "servo" / "public_api.py").resolve(),
34
+ )
35
+ contract_path = next((path for path in candidates if path.is_file()), None)
36
+ if contract_path is None:
37
+ raise RuntimeError("servo-client build could not locate src/servo/public_api.py")
38
+ tree = ast.parse(contract_path.read_text(encoding="utf-8"), filename=str(contract_path))
39
+ values: dict[str, tuple[str, ...]] = {}
40
+
41
+ def evaluate(node: ast.expr) -> tuple[str, ...] | None:
42
+ if isinstance(node, ast.Name):
43
+ return values.get(node.id)
44
+ if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
45
+ left = evaluate(node.left)
46
+ right = evaluate(node.right)
47
+ return left + right if left is not None and right is not None else None
48
+ try:
49
+ value = ast.literal_eval(node)
50
+ except (ValueError, TypeError):
51
+ return None
52
+ if isinstance(value, tuple) and all(isinstance(item, str) for item in value):
53
+ return value
54
+ return None
55
+
56
+ for node in tree.body:
57
+ if not isinstance(node, ast.Assign) or len(node.targets) != 1:
58
+ continue
59
+ target = node.targets[0]
60
+ if not isinstance(target, ast.Name):
61
+ continue
62
+ value = evaluate(node.value)
63
+ if value is not None:
64
+ values[target.id] = value
65
+ try:
66
+ return values[name]
67
+ except KeyError as exc:
68
+ raise RuntimeError(f"servo-client public contract does not define {name}") from exc
69
+
70
+
71
+ _PUBLIC_PACKAGES = _contract_value("DISTRIBUTION_PACKAGES")
72
+ _CLIENT_EXCLUDED_PATHS = {
73
+ Path(*module.split(".")).with_suffix(".py")
74
+ for module in _contract_value("CLIENT_EXCLUDED_MODULES")
75
+ }
76
+ _STAGE_EXCLUDES = {
77
+ ".git",
78
+ ".pytest_cache",
79
+ ".ruff_cache",
80
+ ".venv",
81
+ "__pycache__",
82
+ "build",
83
+ "dist",
84
+ "src",
85
+ }
86
+ _PUBLIC_BUILD_ARTIFACT_SUFFIXES = {".dylib", ".pyd", ".so"}
87
+ _BENCHMARK_ONLY_SOURCES = {
88
+ "flatbuffers_cython.c",
89
+ "flatbuffers_cython.pyx",
90
+ }
91
+ _PLATFORM_TAG_ENV_VAR = "SERVO_CLIENT_WHEEL_PLATFORM_TAG"
92
+ _RELEASE_PLATFORM_TAGS = {
93
+ "manylinux_2_28_aarch64",
94
+ "manylinux_2_28_x86_64",
95
+ }
96
+
97
+
98
+ def _canonical_source_root() -> Path:
99
+ local_source = _PROJECT_ROOT / "src"
100
+ repository_source = (_PROJECT_ROOT / ".." / ".." / "src").resolve()
101
+ for candidate in (local_source, repository_source):
102
+ if all((candidate / package).is_dir() for package in _PUBLIC_PACKAGES):
103
+ return candidate
104
+ raise RuntimeError(
105
+ "servo-client build requires all configured public source packages: "
106
+ + ", ".join(_PUBLIC_PACKAGES)
107
+ )
108
+
109
+
110
+ def _ignore_stage_entry(path: Path) -> bool:
111
+ return (
112
+ path.name in _STAGE_EXCLUDES
113
+ or path.name.endswith(".egg-info")
114
+ or path.suffix in {".pyc", ".pyo"}
115
+ )
116
+
117
+
118
+ def _ignore_public_source_entry(path: Path, *, source_root: Path) -> bool:
119
+ """Exclude local/benchmark build products from the public SDK projection.
120
+
121
+ The canonical source tree is also the output directory for optional native builds. Copying it
122
+ wholesale can otherwise put a developer's architecture-specific extension into a supposedly
123
+ portable wheel. The FlatBuffers Cython source is benchmark-only and is not a product codec.
124
+ The one native executable that is part of the product is suffixless and remains included.
125
+ """
126
+ return (
127
+ _ignore_stage_entry(path)
128
+ or path.relative_to(source_root) in _CLIENT_EXCLUDED_PATHS
129
+ or path.suffix in _PUBLIC_BUILD_ARTIFACT_SUFFIXES
130
+ or path.name in _BENCHMARK_ONLY_SOURCES
131
+ )
132
+
133
+
134
+ def _copy_project(stage: Path) -> None:
135
+ stage.mkdir(parents=True)
136
+ for source in _PROJECT_ROOT.iterdir():
137
+ if _ignore_stage_entry(source):
138
+ continue
139
+ destination = stage / source.name
140
+ if source.is_dir():
141
+ shutil.copytree(
142
+ source,
143
+ destination,
144
+ ignore=lambda _directory, names: [
145
+ name for name in names if _ignore_stage_entry(Path(name))
146
+ ],
147
+ )
148
+ else:
149
+ shutil.copy2(source, destination)
150
+
151
+ canonical_source = _canonical_source_root()
152
+ staged_source = stage / "src"
153
+ staged_source.mkdir()
154
+ for package in _PUBLIC_PACKAGES:
155
+ shutil.copytree(
156
+ canonical_source / package,
157
+ staged_source / package,
158
+ ignore=lambda directory, names: [
159
+ name
160
+ for name in names
161
+ if _ignore_public_source_entry(
162
+ Path(directory) / name,
163
+ source_root=canonical_source,
164
+ )
165
+ ],
166
+ )
167
+
168
+
169
+ def _native_artifact(stage: Path) -> Path:
170
+ return stage / "src" / "servo" / "_native" / "servo-action-session-quic"
171
+
172
+
173
+ def _exclude_native_artifact(stage: Path) -> None:
174
+ """Keep the host architecture's executable out of source distributions."""
175
+ artifact = _native_artifact(stage)
176
+ if artifact.is_file():
177
+ artifact.unlink()
178
+
179
+
180
+ def _configure_platform_wheel(stage: Path) -> None:
181
+ """Make a wheel carrying the native data plane fail on another architecture."""
182
+ if not _native_artifact(stage).is_file():
183
+ return
184
+ platform_override = os.environ.get(_PLATFORM_TAG_ENV_VAR)
185
+ if platform_override is not None and platform_override not in _RELEASE_PLATFORM_TAGS:
186
+ raise RuntimeError(
187
+ f"{_PLATFORM_TAG_ENV_VAR} must name a governed release platform, "
188
+ f"got {platform_override!r}"
189
+ )
190
+ (stage / "setup.py").write_text(
191
+ f"""from setuptools import Distribution, setup
192
+ from wheel.bdist_wheel import bdist_wheel
193
+
194
+
195
+ PLATFORM_OVERRIDE = {platform_override!r}
196
+
197
+
198
+ class PlatformDistribution(Distribution):
199
+ def has_ext_modules(self):
200
+ return True
201
+
202
+
203
+ class PlatformWheel(bdist_wheel):
204
+ def finalize_options(self):
205
+ super().finalize_options()
206
+ self.root_is_pure = False
207
+
208
+ def get_tag(self):
209
+ _python, _abi, platform = super().get_tag()
210
+ return "py3", "none", PLATFORM_OVERRIDE or platform
211
+
212
+
213
+ setup(distclass=PlatformDistribution, cmdclass={{"bdist_wheel": PlatformWheel}})
214
+ """,
215
+ encoding="utf-8",
216
+ )
217
+
218
+
219
+ def _configure_editable_project(stage: Path) -> None:
220
+ """Force setuptools' selective finder while retaining canonical source paths.
221
+
222
+ A single ``{"": "src"}`` mapping makes setuptools emit a plain ``.pth`` entry. Besides
223
+ pointing at the temporary staging tree, that would expose every repository package. Explicit
224
+ top-level mappings make setuptools emit its selective meta-path finder, and the finder resolves
225
+ directly to the canonical source directories after the build staging tree is removed.
226
+ """
227
+
228
+ canonical_source = _canonical_source_root()
229
+ mappings = ", ".join(
230
+ f'"{package}" = "{Path(os.path.relpath(canonical_source / package, stage)).as_posix()}"'
231
+ for package in _PUBLIC_PACKAGES
232
+ )
233
+ pyproject_path = stage / "pyproject.toml"
234
+ pyproject = pyproject_path.read_text(encoding="utf-8")
235
+ original = 'package-dir = {"" = "src"}'
236
+ replacement = f"package-dir = {{{mappings}}}"
237
+ if pyproject.count(original) != 1:
238
+ raise RuntimeError("servo-client editable build could not locate package-dir declaration")
239
+ pyproject_path.write_text(pyproject.replace(original, replacement), encoding="utf-8")
240
+
241
+
242
+ @contextmanager
243
+ def _staged_project(
244
+ *,
245
+ editable: bool = False,
246
+ include_native: bool = True,
247
+ platform_wheel: bool = False,
248
+ ) -> Iterator[None]:
249
+ previous_directory = Path.cwd()
250
+ with tempfile.TemporaryDirectory(prefix="servo-client-build-") as temporary_directory:
251
+ # Resolve macOS' /var -> /private/var symlink before deriving editable
252
+ # package paths. A lexical relative path calculated across that symlink
253
+ # otherwise resolves under /private/Users after setuptools changes cwd.
254
+ stage = Path(temporary_directory).resolve() / "servo-client"
255
+ _copy_project(stage)
256
+ if not include_native:
257
+ _exclude_native_artifact(stage)
258
+ if platform_wheel:
259
+ _configure_platform_wheel(stage)
260
+ if editable:
261
+ _configure_editable_project(stage)
262
+ os.chdir(stage)
263
+ try:
264
+ yield
265
+ finally:
266
+ os.chdir(previous_directory)
267
+
268
+
269
+ def _absolute_directory(path: str) -> str:
270
+ return str(Path(path).resolve())
271
+
272
+
273
+ def get_requires_for_build_wheel(
274
+ config_settings: Mapping[str, Any] | None = None,
275
+ ) -> list[str]:
276
+ with _staged_project(platform_wheel=True):
277
+ return _setuptools.get_requires_for_build_wheel(config_settings)
278
+
279
+
280
+ def get_requires_for_build_sdist(
281
+ config_settings: Mapping[str, Any] | None = None,
282
+ ) -> list[str]:
283
+ with _staged_project(include_native=False):
284
+ return _setuptools.get_requires_for_build_sdist(config_settings)
285
+
286
+
287
+ def get_requires_for_build_editable(
288
+ config_settings: Mapping[str, Any] | None = None,
289
+ ) -> list[str]:
290
+ with _staged_project(editable=True):
291
+ return _setuptools.get_requires_for_build_editable(config_settings)
292
+
293
+
294
+ def prepare_metadata_for_build_wheel(
295
+ metadata_directory: str,
296
+ config_settings: Mapping[str, Any] | None = None,
297
+ ) -> str:
298
+ metadata_directory = _absolute_directory(metadata_directory)
299
+ with _staged_project(platform_wheel=True):
300
+ return _setuptools.prepare_metadata_for_build_wheel(
301
+ metadata_directory,
302
+ config_settings,
303
+ )
304
+
305
+
306
+ def prepare_metadata_for_build_editable(
307
+ metadata_directory: str,
308
+ config_settings: Mapping[str, Any] | None = None,
309
+ ) -> str:
310
+ metadata_directory = _absolute_directory(metadata_directory)
311
+ with _staged_project(editable=True):
312
+ return _setuptools.prepare_metadata_for_build_editable(
313
+ metadata_directory,
314
+ config_settings,
315
+ )
316
+
317
+
318
+ def build_wheel(
319
+ wheel_directory: str,
320
+ config_settings: Mapping[str, Any] | None = None,
321
+ metadata_directory: str | None = None,
322
+ ) -> str:
323
+ wheel_directory = _absolute_directory(wheel_directory)
324
+ if metadata_directory is not None:
325
+ metadata_directory = _absolute_directory(metadata_directory)
326
+ with _staged_project(platform_wheel=True):
327
+ return _setuptools.build_wheel(
328
+ wheel_directory,
329
+ config_settings,
330
+ metadata_directory,
331
+ )
332
+
333
+
334
+ def build_editable(
335
+ wheel_directory: str,
336
+ config_settings: Mapping[str, Any] | None = None,
337
+ metadata_directory: str | None = None,
338
+ ) -> str:
339
+ wheel_directory = _absolute_directory(wheel_directory)
340
+ if metadata_directory is not None:
341
+ metadata_directory = _absolute_directory(metadata_directory)
342
+ with _staged_project(editable=True):
343
+ return _setuptools.build_editable(
344
+ wheel_directory,
345
+ config_settings,
346
+ metadata_directory,
347
+ )
348
+
349
+
350
+ def build_sdist(
351
+ sdist_directory: str,
352
+ config_settings: Mapping[str, Any] | None = None,
353
+ ) -> str:
354
+ sdist_directory = _absolute_directory(sdist_directory)
355
+ with _staged_project(include_native=False):
356
+ return _setuptools.build_sdist(sdist_directory, config_settings)
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+