envloop-cli 1.13.0__tar.gz → 1.16.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 (36) hide show
  1. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/PKG-INFO +26 -3
  2. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/README.pypi.md +25 -2
  3. envloop_cli-1.16.0/envloop/__init__.py +6 -0
  4. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/auth_cli.py +4 -2
  5. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/auth_login.py +5 -3
  6. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/cli.py +16 -7
  7. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/cli_errors.py +1 -0
  8. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/client.py +2 -0
  9. envloop_cli-1.16.0/envloop/decision.py +19 -0
  10. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/http.py +33 -5
  11. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/inference.py +1 -1
  12. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/pyproject.toml +1 -1
  13. envloop_cli-1.13.0/envloop/__init__.py +0 -5
  14. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/.gitignore +0 -0
  15. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/agent_runs.py +0 -0
  16. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/auth_workspace.py +0 -0
  17. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/cli_output.py +0 -0
  18. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/config.py +0 -0
  19. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/job_logs.py +0 -0
  20. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/job_spec_cli.py +0 -0
  21. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/lifecycle_cli.py +0 -0
  22. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/navigation_resources.py +0 -0
  23. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/profile_secrets.py +0 -0
  24. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/profile_updates.py +0 -0
  25. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/resource_cli.py +0 -0
  26. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/resources.py +0 -0
  27. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/rsi_cli.py +0 -0
  28. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/sandbox.py +0 -0
  29. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/terminal.py +0 -0
  30. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/train.py +0 -0
  31. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/train_log_cli.py +0 -0
  32. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/train_progress.py +0 -0
  33. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/train_runner_cli.py +0 -0
  34. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/training/__init__.py +0 -0
  35. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/utils/__init__.py +0 -0
  36. {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/utils/logging.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: envloop-cli
3
- Version: 1.13.0
3
+ Version: 1.16.0
4
4
  Summary: EnvLoop CLI and Python SDK for jobs, training, checkpoints and inference
5
5
  Project-URL: Homepage, https://envloop.ai
6
6
  Project-URL: Documentation, https://docs.envloop.ai
@@ -192,8 +192,8 @@ Choose a published Runner readable in your project. Save this request as `gpu-ru
192
192
  {
193
193
  "request_id": "gpu-run-001",
194
194
  "trials": [
195
- {"name": "first", "spec": {"runner": "envhub/bash-runner@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}},
196
- {"name": "second", "spec": {"runner": "envhub/bash-runner@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}}
195
+ {"name": "first", "spec": {"runner": "envhub/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}},
196
+ {"name": "second", "spec": {"runner": "envhub/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}}
197
197
  ]
198
198
  }
199
199
  ```
@@ -217,3 +217,26 @@ Use `el --no-color job list` or append `--no-color` to a command to disable high
217
217
  `NO_COLOR=1`, `TERM=dumb`, pipes, and redirected output also disable colors. JSON preserves original values.
218
218
  List times use the local timezone; Job logs retain the recorded timezone, with full precision and offsets available through `--json`.
219
219
  `el job run live RUN --follow --json` emits complete response frames as NDJSON; Ctrl-C stops reading without stopping the Run.
220
+
221
+ ## Job-bound decision plugins
222
+
223
+ When the platform and published Runner support Job Profiles, include the authorized
224
+ `profile` snapshot in the Train or Inference request's `spec`. The platform freezes
225
+ its inputs, recipe and approved plugin pins. This does not select an executor or provider.
226
+
227
+ For an Inference Job with a bound decision plugin, save the plugin payload as JSON:
228
+
229
+ ```bash
230
+ el job decision JOB_ID --model WORKSPACE/MODEL --spec payload.json
231
+ ```
232
+
233
+ ```python
234
+ with Client.remote() as client:
235
+ result = client.decision.evaluate(
236
+ "JOB_ID", model="WORKSPACE/MODEL", payload=payload
237
+ )
238
+ ```
239
+
240
+ Use the exact model returned by the Job. The SDK forwards the payload to the
241
+ Job's `/Inference/decision` API; the approved plugin validates its business fields.
242
+ Availability requires matching platform support and approved content in your project.
@@ -178,8 +178,8 @@ Choose a published Runner readable in your project. Save this request as `gpu-ru
178
178
  {
179
179
  "request_id": "gpu-run-001",
180
180
  "trials": [
181
- {"name": "first", "spec": {"runner": "envhub/bash-runner@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}},
182
- {"name": "second", "spec": {"runner": "envhub/bash-runner@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}}
181
+ {"name": "first", "spec": {"runner": "envhub/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}},
182
+ {"name": "second", "spec": {"runner": "envhub/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}}
183
183
  ]
184
184
  }
185
185
  ```
@@ -203,3 +203,26 @@ Use `el --no-color job list` or append `--no-color` to a command to disable high
203
203
  `NO_COLOR=1`, `TERM=dumb`, pipes, and redirected output also disable colors. JSON preserves original values.
204
204
  List times use the local timezone; Job logs retain the recorded timezone, with full precision and offsets available through `--json`.
205
205
  `el job run live RUN --follow --json` emits complete response frames as NDJSON; Ctrl-C stops reading without stopping the Run.
206
+
207
+ ## Job-bound decision plugins
208
+
209
+ When the platform and published Runner support Job Profiles, include the authorized
210
+ `profile` snapshot in the Train or Inference request's `spec`. The platform freezes
211
+ its inputs, recipe and approved plugin pins. This does not select an executor or provider.
212
+
213
+ For an Inference Job with a bound decision plugin, save the plugin payload as JSON:
214
+
215
+ ```bash
216
+ el job decision JOB_ID --model WORKSPACE/MODEL --spec payload.json
217
+ ```
218
+
219
+ ```python
220
+ with Client.remote() as client:
221
+ result = client.decision.evaluate(
222
+ "JOB_ID", model="WORKSPACE/MODEL", payload=payload
223
+ )
224
+ ```
225
+
226
+ Use the exact model returned by the Job. The SDK forwards the payload to the
227
+ Job's `/Inference/decision` API; the approved plugin validates its business fields.
228
+ Availability requires matching platform support and approved content in your project.
@@ -0,0 +1,6 @@
1
+ """EnvLoop Python client."""
2
+
3
+ from .client import Client
4
+ from .decision import DecisionClient
5
+
6
+ __all__ = ["Client", "DecisionClient"]
@@ -130,7 +130,8 @@ def _verify_key(url, key, workspace_id=None, project_id=None):
130
130
  response = HttpService(url, timeout=5, api_key=key, project_id=project_id, config=False,
131
131
  environment=False).request("GET", "/v1/me")
132
132
  except RemoteError as error:
133
- code = {401: "AUTHENTICATION_REJECTED", 403: "AUTHORIZATION_DENIED"}.get(error.status_code)
133
+ code = "EDGE_REJECTED" if error.code == "EDGE_REJECTED" else {
134
+ 401: "AUTHENTICATION_REJECTED", 403: "AUTHORIZATION_DENIED"}.get(error.status_code)
134
135
  if code is None:
135
136
  code = error.code if error.status_code is None and error.code in {
136
137
  "NETWORK_ERROR", "INVALID_RESPONSE", "RESPONSE_TOO_LARGE",
@@ -279,7 +280,8 @@ def run_auth(args, parser):
279
280
  except (ConfigError, RemoteError) as error:
280
281
  from .cli_errors import format_error, safe_error_code
281
282
 
282
- report["check"] = {401: "authentication_rejected", 403: "authorization_denied"}.get(
283
+ report["check"] = "edge_rejected" if getattr(error, "code", None) == "EDGE_REJECTED" else {
284
+ 401: "authentication_rejected", 403: "authorization_denied"}.get(
283
285
  getattr(error, "status_code", None), safe_error_code(error))
284
286
  _print(report, args.json)
285
287
  print(format_error(error), file=sys.stderr)
@@ -7,18 +7,18 @@ import webbrowser
7
7
  from http.client import InvalidURL
8
8
  from urllib.error import HTTPError, URLError
9
9
  from urllib.parse import parse_qs, urlsplit
10
- from urllib.request import ProxyHandler, Request, build_opener
10
+ from urllib.request import Request
11
11
 
12
12
  from .auth_workspace import _identity
13
13
  from .config import ConfigError, normalize_url
14
- from .http import NoRedirect
14
+ from .http import build_client_opener, error_fields
15
15
 
16
16
 
17
17
  def _request(url, action, body, timeout):
18
18
  request = Request(url + '/v1/auth/cli/' + action, data=json.dumps(body).encode(),
19
19
  headers={'Content-Type': 'application/json'}, method='POST')
20
20
  try:
21
- with build_opener(ProxyHandler(), NoRedirect()).open(request, timeout=timeout) as response:
21
+ with build_client_opener().open(request, timeout=timeout) as response:
22
22
  raw = response.read(8193)
23
23
  if len(raw) > 8192:
24
24
  raise ConfigError('AUTH_LOGIN_INVALID_RESPONSE')
@@ -27,6 +27,8 @@ def _request(url, action, body, timeout):
27
27
  raise ConfigError('AUTH_LOGIN_INVALID_RESPONSE')
28
28
  return result
29
29
  except HTTPError as error:
30
+ if error.code == 403 and error_fields(error)['code'] == 'EDGE_REJECTED':
31
+ raise ConfigError('EDGE_REJECTED') from None
30
32
  code = {403: 'AUTHORIZATION_DENIED', 409: 'AUTH_LOGIN_UNAVAILABLE', 410: 'AUTH_LOGIN_EXPIRED',
31
33
  429: 'AUTH_LOGIN_BUSY', 503: 'AUTH_LOGIN_UNAVAILABLE'}.get(error.code, 'AUTH_LOGIN_HTTP_ERROR')
32
34
  raise ConfigError(code) from None
@@ -6,16 +6,10 @@ import math
6
6
  import os
7
7
  import shlex
8
8
  import sys
9
- from importlib.metadata import PackageNotFoundError, version
10
9
  from pathlib import Path
11
10
 
12
11
  from .client import Client
13
-
14
- try:
15
- CLI_VERSION = version("envloop-cli")
16
- except PackageNotFoundError:
17
- CLI_VERSION = "unknown"
18
-
12
+ from .http import CLI_VERSION
19
13
 
20
14
  SECTIONS = {
21
15
  'OVERVIEW': ('dashboard',),
@@ -210,6 +204,10 @@ def parser():
210
204
  if action == 'event':
211
205
  command.add_argument('--json', action='store_true')
212
206
  command.set_defaults(_list_view=True)
207
+ decision = jobs.add_parser("decision", help="Invoke the frozen decision plugin of an Inference Job")
208
+ decision.add_argument("job_id")
209
+ decision.add_argument("--model", required=True)
210
+ decision.add_argument("--spec", type=Path, required=True, metavar="PAYLOAD.json")
213
211
  from .resource_cli import add_job_resources
214
212
  add_job_resources(jobs)
215
213
  listing = jobs.add_parser("list")
@@ -434,6 +432,15 @@ def _main(argv=None):
434
432
 
435
433
  print(format_error(exc), file=sys.stderr)
436
434
  return 1
435
+ if args.group == "job" and args.action == "decision":
436
+ from .decision import DecisionClient
437
+ try:
438
+ if args.spec.stat().st_size > 1024 * 1024:
439
+ raise ValueError("Decision payload exceeds 1 MiB")
440
+ args.payload = json.loads(args.spec.read_text(encoding="utf-8"))
441
+ DecisionClient.validate(args.job_id, args.model, args.payload)
442
+ except (OSError, ValueError, UnicodeError):
443
+ cli.error("decision requires a readable JSON object of at most 1 MiB, without model")
437
444
  if args.group == "job" and args.action == "submit":
438
445
  from .train_runner_cli import SHORTCUT_FIELDS, runner_request
439
446
 
@@ -586,6 +593,8 @@ def _main(argv=None):
586
593
  if args.wait:
587
594
  wait_options = {"timeout": args.timeout_sec} if args.type == "train" and args.spec is not None and args.timeout_sec is not None else {}
588
595
  result = client.jobs.wait(result["job_id"], **wait_options)
596
+ elif args.action == "decision":
597
+ result = client.decision.evaluate(args.job_id, model=args.model, payload=args.payload)
589
598
  elif args.action == 'event':
590
599
  result = client.jobs.events(args.job_id)
591
600
  elif args.action in {'serving-profile', 'adapter'}:
@@ -29,6 +29,7 @@ _register("HTTP_ERROR", "The API request failed.")
29
29
  _register("NOT_FOUND RESOURCE_NOT_FOUND JOB_NOT_FOUND RUNNER_NOT_FOUND DATASET_REVISION_NOT_FOUND PROFILE_NOT_FOUND WORKSPACE_NOT_FOUND CHECKPOINT_NOT_FOUND AGENT_RUN_NOT_FOUND ARTIFACT_NOT_FOUND", "The requested API resource or route was not found.")
30
30
  _register("UNAUTHORIZED UNAUTHENTICATED INVALID_AUTH AUTHENTICATION_REQUIRED AUTHENTICATION_REJECTED API_KEY_REQUIRED", "API key authentication is required or was rejected.")
31
31
  _register("FORBIDDEN ACCESS_DENIED AUTHORIZATION_DENIED PERMISSION_DENIED WORKSPACE_ACCESS_DENIED PROJECT_ACCESS_DENIED", "The current credentials do not have permission for this operation.")
32
+ _register("EDGE_REJECTED", "The network edge (Cloudflare) rejected the request before it reached the EnvLoop API; this is not a permission decision. Upgrade envloop-cli, check any proxy or custom User-Agent, or contact support with the HTTP status.")
32
33
  _register("HUMAN_SESSION_REQUIRED WORKSPACE_LIST_REQUIRES_SESSION PROJECT_LIST_REQUIRES_SESSION", "This operation requires a browser session authorized by the server.")
33
34
  _register("CONFLICT VERSION_CONFLICT RESOURCE_VERSION_CONFLICT PRECONDITION_FAILED IDEMPOTENCY_CONFLICT", "The request conflicts with the current resource version or an earlier request.")
34
35
  _register("BAD_REQUEST VALIDATION_ERROR VALIDATION_FAILED INVALID_REQUEST INVALID_ARGUMENT INVALID_CURSOR INVALID_PROFILE INVALID_PROFILE_SECRETS", "The request or input is invalid.")
@@ -621,6 +621,7 @@ class Client:
621
621
  """A client for a caller-owned service, or use local() for an owned server."""
622
622
 
623
623
  def __init__(self, server: RpcService) -> None:
624
+ from .decision import DecisionClient
624
625
  from .navigation_resources import (
625
626
  Billing,
626
627
  Checkpoints,
@@ -632,6 +633,7 @@ class Client:
632
633
  from .resources import Adapters, Endpoints, Operations
633
634
 
634
635
  self._transport = server
636
+ self.decision = DecisionClient(server)
635
637
  self.tasks = Tasks(server)
636
638
  self.trials = self.tasks
637
639
  self.profiles = Profiles(server)
@@ -0,0 +1,19 @@
1
+ """Thin Job-bound decision transport. Business logic stays in approved plugins."""
2
+ from urllib.parse import quote
3
+
4
+
5
+ class DecisionClient:
6
+ def __init__(self, transport):
7
+ self.transport = transport
8
+
9
+ @staticmethod
10
+ def validate(job_id, model, payload):
11
+ if (not isinstance(job_id, str) or not job_id.strip()
12
+ or not isinstance(model, str) or not model.strip()
13
+ or not isinstance(payload, dict) or 'model' in payload):
14
+ raise ValueError('decision requires job_id, model and a payload object without model')
15
+
16
+ def evaluate(self, job_id, *, model, payload):
17
+ self.validate(job_id, model, payload)
18
+ return self.transport.request('POST', f'/v1/job/{quote(job_id, safe="")}/Inference/decision',
19
+ {'model': model, **payload})
@@ -3,10 +3,12 @@
3
3
  import base64
4
4
  import json
5
5
  import math
6
+ import platform
6
7
  import re
7
8
  from datetime import datetime, timezone
8
9
  from email.utils import parsedate_to_datetime
9
10
  from http.client import HTTPException, InvalidURL
11
+ from importlib.metadata import PackageNotFoundError, version
10
12
  from urllib.error import HTTPError, URLError
11
13
  from urllib.parse import quote, urlencode, urljoin, urlsplit
12
14
  from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener
@@ -66,6 +68,23 @@ class NoRedirect(HTTPRedirectHandler):
66
68
  return None
67
69
 
68
70
 
71
+ try:
72
+ CLI_VERSION = version("envloop-cli")
73
+ except PackageNotFoundError:
74
+ CLI_VERSION = "unknown"
75
+
76
+ # Cloudflare bot rules answer urllib's default "Python-urllib/3.x" with error 1010 (staging workers.dev).
77
+ USER_AGENT = (f"envloop-cli/{CLI_VERSION} (python/{platform.python_version()}; "
78
+ f"{'-'.join(filter(None, (platform.system().lower(), platform.machine().lower()))) or 'unknown'})")
79
+
80
+
81
+ def build_client_opener():
82
+ """Proxy-aware opener that never follows redirects and names the client in every request."""
83
+ opener = build_opener(ProxyHandler(), NoRedirect())
84
+ opener.addheaders = [("User-Agent", USER_AGENT)]
85
+ return opener
86
+
87
+
69
88
  def _text(value, limit, secrets=()):
70
89
  """One printable line: no control characters, no submitted secret or URL query, bounded length."""
71
90
  if not isinstance(value, str):
@@ -101,10 +120,12 @@ def error_fields(error, secrets=()):
101
120
  Reads top-level `code/message/hint/request_id/fields/details` or the same
102
121
  keys nested in `error: {...}`; request_id falls back to X-Request-ID.
103
122
  """
123
+ raw, parsed = b"", True
104
124
  try:
105
- data = json.loads(error.read(8192))
125
+ raw = error.read(8192)
126
+ data = json.loads(raw)
106
127
  except (ValueError, UnicodeError, OSError, HTTPException):
107
- data = None
128
+ data, parsed = None, False
108
129
  finally:
109
130
  error.close()
110
131
  data = data if isinstance(data, dict) else {}
@@ -115,6 +136,13 @@ def error_fields(error, secrets=()):
115
136
  return nested[key] if key in nested else data.get(key)
116
137
 
117
138
  code = pick("code")
139
+ # The platform answers in JSON. A 403 whose body is non-JSON text (Cloudflare's "error code: 1010") or carries
140
+ # Cloudflare's numeric error_code was produced by the edge before the API saw the credential: not an RBAC
141
+ # decision. A JSON 403 without a code, even an empty one, stays a platform authorization answer.
142
+ edge_text = not parsed and bool(raw.strip())
143
+ edge_code = isinstance(data.get("error_code"), int) and not isinstance(data.get("error_code"), bool)
144
+ if error.code == 403 and not isinstance(code, str) and (edge_text or edge_code):
145
+ code = "EDGE_REJECTED"
118
146
  if (not isinstance(code, str) or not code or len(code) > 128 or not code.isascii()
119
147
  or not all("A" <= character <= "Z" or character.isdigit() or character == "_" for character in code)):
120
148
  code = "HTTP_ERROR"
@@ -169,7 +197,7 @@ class HttpService:
169
197
  self.base = connection.api_url
170
198
  self.secret = connection.api_key
171
199
  self.timeout = timeout
172
- self.opener = build_opener(ProxyHandler(), NoRedirect())
200
+ self.opener = build_client_opener()
173
201
 
174
202
  def remote_error(self, error, body=None, **options):
175
203
  submitted = body.get("secrets") if isinstance(body, dict) else None
@@ -210,7 +238,7 @@ class HttpService:
210
238
  or parts.username is not None or parts.password is not None or parts.fragment):
211
239
  raise RemoteError('UNSAFE_ARTIFACT_REDIRECT')
212
240
  try:
213
- return Download(build_opener(ProxyHandler(), NoRedirect()).open(url, timeout=self.timeout))
241
+ return Download(build_client_opener().open(url, timeout=self.timeout))
214
242
  except HTTPError as error:
215
243
  error.close()
216
244
  raise RemoteError('ARTIFACT_DOWNLOAD_FAILED', error.code) from None
@@ -309,7 +337,7 @@ class HttpService:
309
337
  # A presigned object URL carries its own authority: use a separate opener
310
338
  # with no Authorization / workspace / project headers and no further hops.
311
339
  try:
312
- return Download(build_opener(ProxyHandler(), NoRedirect()).open(target, timeout=self.timeout))
340
+ return Download(build_client_opener().open(target, timeout=self.timeout))
313
341
  except HTTPError as error:
314
342
  error.close()
315
343
  raise RemoteError("ARTIFACT_DOWNLOAD_FAILED", error.code) from None
@@ -2,7 +2,7 @@
2
2
  import re
3
3
 
4
4
  _FIELDS = {'name', 'runner', 'inputs', 'serving_profile', 'hardware', 'weights_format',
5
- 'scaling', 'temporary_scope_id'}
5
+ 'scaling', 'temporary_scope_id', 'profile'}
6
6
 
7
7
 
8
8
  def validate_submission(body):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "envloop-cli"
3
- version = "1.13.0"
3
+ version = "1.16.0"
4
4
  description = "EnvLoop CLI and Python SDK for jobs, training, checkpoints and inference"
5
5
  requires-python = ">=3.11"
6
6
  readme = "README.pypi.md"
@@ -1,5 +0,0 @@
1
- """EnvLoop Python client."""
2
-
3
- from .client import Client
4
-
5
- __all__ = ["Client"]
File without changes