cuiman 0.2.1.dev1__tar.gz → 0.3.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 (72) hide show
  1. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/.gitignore +6 -2
  2. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/PKG-INFO +8 -5
  3. cuiman-0.3.0/cuiman/__init__.py +42 -0
  4. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/async_client.py +43 -38
  5. cuiman-0.3.0/cuiman/api/async_client_mixin.py +319 -0
  6. cuiman-0.3.0/cuiman/api/auth/__init__.py +35 -0
  7. cuiman-0.3.0/cuiman/api/auth/config.py +293 -0
  8. cuiman-0.3.0/cuiman/api/auth/interactive.py +101 -0
  9. cuiman-0.3.0/cuiman/api/auth/login.py +73 -0
  10. cuiman-0.3.0/cuiman/api/auth/oauth2_client.py +75 -0
  11. cuiman-0.3.0/cuiman/api/auth/oidc.py +167 -0
  12. cuiman-0.3.0/cuiman/api/auth/secret_store.py +185 -0
  13. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/client.py +43 -38
  14. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/client_app_mixin.py +12 -2
  15. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/client_mixin.py +132 -11
  16. cuiman-0.3.0/cuiman/api/client_mixin_base.py +290 -0
  17. cuiman-0.3.0/cuiman/api/config.py +618 -0
  18. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/defaults.py +1 -1
  19. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/ishell.py +1 -2
  20. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/opener/context.py +8 -8
  21. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/opener/opener.py +4 -4
  22. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/opener/registry.py +3 -0
  23. cuiman-0.2.1.dev1/cuiman/api/transport/httpx.py → cuiman-0.3.0/cuiman/api/transport/httpx2.py +44 -40
  24. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/app/display.py +8 -1
  25. cuiman-0.3.0/cuiman/app/dist/assets/index-Br7nJ6aR.js +85 -0
  26. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/app/dist/index.html +1 -1
  27. cuiman-0.3.0/cuiman/app/launch.py +347 -0
  28. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/app/notebook-display.js +32 -11
  29. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/app/serve.py +41 -8
  30. cuiman-0.3.0/cuiman/app/url.py +60 -0
  31. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/cli/cli.py +142 -49
  32. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/cli/client.py +36 -1
  33. cuiman-0.3.0/cuiman/cli/config.py +223 -0
  34. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/pyproject.toml +8 -6
  35. cuiman-0.2.1.dev1/cuiman/__init__.py +0 -23
  36. cuiman-0.2.1.dev1/cuiman/api/async_client_mixin.py +0 -153
  37. cuiman-0.2.1.dev1/cuiman/api/auth/__init__.py +0 -16
  38. cuiman-0.2.1.dev1/cuiman/api/auth/config.py +0 -161
  39. cuiman-0.2.1.dev1/cuiman/api/auth/login.py +0 -180
  40. cuiman-0.2.1.dev1/cuiman/api/auth/login_async.py +0 -62
  41. cuiman-0.2.1.dev1/cuiman/api/config.py +0 -204
  42. cuiman-0.2.1.dev1/cuiman/app/dist/assets/index-9NSgOSt1.js +0 -85
  43. cuiman-0.2.1.dev1/cuiman/app/service.py +0 -116
  44. cuiman-0.2.1.dev1/cuiman/app/url.py +0 -77
  45. cuiman-0.2.1.dev1/cuiman/cli/config.py +0 -189
  46. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/LICENSE +0 -0
  47. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/README.md +0 -0
  48. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/__init__.py +0 -0
  49. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/exceptions.py +0 -0
  50. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/jobs.py +0 -0
  51. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/opener/__init__.py +0 -0
  52. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/opener/errors.py +0 -0
  53. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/opener/impl/__init__.py +0 -0
  54. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/opener/impl/_gpd.py +0 -0
  55. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/opener/impl/_image.py +0 -0
  56. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/opener/impl/_pd.py +0 -0
  57. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/opener/impl/_xr.py +0 -0
  58. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/opener/impl/base.py +0 -0
  59. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/opener/impl/openers.py +0 -0
  60. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/service_client.py +0 -0
  61. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/transport/__init__.py +0 -0
  62. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/transport/args.py +0 -0
  63. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/api/transport/transport.py +0 -0
  64. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/app/__init__.py +0 -0
  65. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/app/app.py +0 -0
  66. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/app/dist/assets/index-DKE7nU5k.css +0 -0
  67. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/app/dist/eozilla-small.svg +0 -0
  68. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/app/dist/eozilla.svg +0 -0
  69. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/app/dist/privacy.md +0 -0
  70. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/cli/__init__.py +0 -0
  71. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/cli/output.py +0 -0
  72. {cuiman-0.2.1.dev1 → cuiman-0.3.0}/cuiman/py.typed +0 -0
@@ -1,9 +1,10 @@
1
1
  # Testing
2
2
  *.zarr/
3
3
  .cov-report
4
+ notebooks/auth-test.ipynb
4
5
 
5
- # Documentation generation
6
- docs/notebooks/*.ipynb
6
+ # Legacy generated documentation (excluded from the site)
7
+ docs/notebooks/
7
8
 
8
9
  # Byte-compiled / optimized / DLL files
9
10
  __pycache__/
@@ -202,3 +203,6 @@ cython_debug/
202
203
  # refer to https://docs.cursor.com/context/ignore-files
203
204
  .cursorignore
204
205
  .cursorindexingignore
206
+
207
+ # Downloaded font assets
208
+ docs/assets/fonts/*.ttf
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cuiman
3
- Version: 0.2.1.dev1
3
+ Version: 0.3.0
4
4
  Summary: Provides a client Python API, GUI, and CLI for servers compliant with OGC API - Processes
5
5
  Project-URL: Documentation, https://eo-tools.github.io/eozilla
6
6
  Project-URL: Repository, https://github.com/eo-tools/eozilla
@@ -27,11 +27,14 @@ Classifier: Topic :: Scientific/Engineering
27
27
  Classifier: Topic :: Software Development
28
28
  Classifier: Typing :: Typed
29
29
  Requires-Python: >=3.10
30
- Requires-Dist: gavicore>=0.2.1.dev1
31
- Requires-Dist: httpx
30
+ Requires-Dist: authlib<1.9,>=1.8
31
+ Requires-Dist: gavicore>=0.3.0
32
+ Requires-Dist: httpx2
32
33
  Requires-Dist: ipython
33
- Requires-Dist: pydantic
34
- Requires-Dist: pydantic-settings
34
+ Requires-Dist: joserfc<2,>=1.6
35
+ Requires-Dist: keyring
36
+ Requires-Dist: pydantic-settings>=2.14.2
37
+ Requires-Dist: pydantic>=2.12
35
38
  Requires-Dist: pyyaml
36
39
  Requires-Dist: remotestate<1,>=0.3.4
37
40
  Requires-Dist: typer<1,>=0.26.0
@@ -0,0 +1,42 @@
1
+ # Copyright (c) 2025-2026 by the Eozilla team and contributors
2
+ # Permissions are hereby granted under the terms of the Apache 2.0 License:
3
+ # https://opensource.org/license/apache-2-0.
4
+
5
+ from importlib.metadata import version
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ if TYPE_CHECKING:
9
+ from .api.async_client import AsyncClient as AsyncClient
10
+ from .api.client import Client as Client
11
+ from .api.config import ClientConfig as ClientConfig
12
+ from .api.exceptions import ClientError as ClientError
13
+ from .api.jobs import JobMonitor as JobMonitor
14
+ from .api.jobs import JobOptions as JobOptions
15
+
16
+ __version__ = version("cuiman")
17
+
18
+ __all__ = [
19
+ "AsyncClient",
20
+ "Client",
21
+ "ClientConfig",
22
+ "ClientError",
23
+ "JobMonitor",
24
+ "JobOptions",
25
+ "__version__",
26
+ ]
27
+
28
+
29
+ def __getattr__(name: str) -> Any:
30
+ """Load public API exports only when requested, keeping CLI startup light."""
31
+ if name not in __all__:
32
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
33
+ from importlib import import_module
34
+
35
+ value = getattr(import_module(".api", __name__), name)
36
+ globals()[name] = value
37
+ return value
38
+
39
+
40
+ def __dir__() -> list[str]:
41
+ """Include lazy public API exports in interactive discovery."""
42
+ return sorted(set(globals()) | set(__all__))
@@ -1,6 +1,6 @@
1
1
  # generated by gen_client.py:
2
2
  # filename: async_client.py:
3
- # timestamp: 2026-07-15T17:16:03.217600
3
+ # timestamp: 2026-09-14T06:56:16.079586
4
4
 
5
5
 
6
6
  from typing import Any, Optional
@@ -22,7 +22,6 @@ from .client_app_mixin import ClientAppMixin
22
22
  from .config import ClientConfig
23
23
  from .ishell import has_ishell as _ # noqa F401
24
24
  from .transport import AsyncTransport, TransportArgs
25
- from .transport.httpx import HttpxTransport
26
25
 
27
26
 
28
27
  class AsyncClient(ClientAppMixin, AsyncClientMixin):
@@ -30,10 +29,18 @@ class AsyncClient(ClientAppMixin, AsyncClientMixin):
30
29
  The client API for the web service (asynchronous mode).
31
30
 
32
31
  Args:
33
- config: Optional client configuration object. If given,
34
- other configuration arguments are ignored.
32
+ config: Optional configuration object. Explicit keyword settings override
33
+ its values.
34
+ config_type: Optional application configuration class. It owns the
35
+ settings schema, defaults, environment namespace, and profile path.
36
+ When omitted, a supplied ``config`` object's concrete type is used.
35
37
  config_path: Optional path of the configuration file to be loaded
36
- config_kwargs: Configuration settings as keyword arguments.
38
+ config_kwargs (Any): Configuration overrides, including ``auth``. An auth model
39
+ or a dictionary containing ``auth_type`` replaces previous auth settings,
40
+ even when the type is unchanged. A dictionary without ``auth_type``
41
+ merges into the selected auth configuration, including nested mappings.
42
+ If credentials are still missing, a matching profile's keyring entry may
43
+ supply them; explicitly supplied credentials take precedence.
37
44
  api_url: The service URL of the OGC API - Processes.
38
45
  """
39
46
 
@@ -41,6 +48,7 @@ class AsyncClient(ClientAppMixin, AsyncClientMixin):
41
48
  self,
42
49
  *,
43
50
  config: Optional[ClientConfig] = None,
51
+ config_type: type[ClientConfig] | None = None,
44
52
  config_path: Optional[str] = None,
45
53
  api_url: Optional[str] = None,
46
54
  _debug: bool = False,
@@ -49,23 +57,16 @@ class AsyncClient(ClientAppMixin, AsyncClientMixin):
49
57
  ):
50
58
  self._config = ClientConfig.create(
51
59
  config=config,
60
+ config_type=config_type,
52
61
  config_path=config_path,
53
62
  api_url=api_url,
54
63
  **config_kwargs,
55
64
  )
56
65
  if not self._config.api_url:
57
66
  raise ValueError("Required setting 'api_url' not configured")
58
- self._transport = (
59
- HttpxTransport(
60
- api_url=f"{self._config.api_url.rstrip('/')}/",
61
- headers=self._config.auth_headers,
62
- return_type_map=self._config.return_type_map,
63
- async_token_refresher=self._config._make_async_token_refresher(),
64
- debug=_debug,
65
- )
66
- if _transport is None
67
- else _transport
68
- )
67
+ self._transport = _transport
68
+ self._debug = _debug
69
+ self._init_client_runtime()
69
70
 
70
71
  @property
71
72
  def config(self) -> ClientConfig:
@@ -100,7 +101,8 @@ class AsyncClient(ClientAppMixin, AsyncClientMixin):
100
101
 
101
102
  - `500`: A server error occurred.
102
103
  """
103
- return await self._transport.async_call(
104
+ transport = self._get_transport()
105
+ return await transport.async_call(
104
106
  TransportArgs(
105
107
  path="/",
106
108
  method="get",
@@ -127,8 +129,8 @@ class AsyncClient(ClientAppMixin, AsyncClientMixin):
127
129
  |Dismiss|http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/dismiss|
128
130
 
129
131
  For more information, see [OGC API — Processes — Part 1
130
- Section 7.4](https://docs.ogc.org/is/18-062r2/18-
131
- 062r2.html#sc_conformance_classes).
132
+ Section
133
+ 7.4](https://docs.ogc.org/is/18-062r2/18-062r2.html#sc_conformance_classes).
132
134
 
133
135
 
134
136
  Returns:
@@ -144,7 +146,8 @@ class AsyncClient(ClientAppMixin, AsyncClientMixin):
144
146
 
145
147
  - `500`: A server error occurred.
146
148
  """
147
- return await self._transport.async_call(
149
+ transport = self._get_transport()
150
+ return await transport.async_call(
148
151
  TransportArgs(
149
152
  path="/conformance",
150
153
  method="get",
@@ -172,7 +175,8 @@ class AsyncClient(ClientAppMixin, AsyncClientMixin):
172
175
  ClientError: If the call to the web service fails
173
176
  with a status code != `2xx`.
174
177
  """
175
- return await self._transport.async_call(
178
+ transport = self._get_transport()
179
+ return await transport.async_call(
176
180
  TransportArgs(
177
181
  path="/processes",
178
182
  method="get",
@@ -193,8 +197,8 @@ class AsyncClient(ClientAppMixin, AsyncClientMixin):
193
197
  description.
194
198
 
195
199
  For more information, see [OGC API — Processes — Part 1
196
- Section 7.10](https://docs.ogc.org/is/18-062r2/18-
197
- 062r2.html#sc_process_description).
200
+ Section
201
+ 7.10](https://docs.ogc.org/is/18-062r2/18-062r2.html#sc_process_description).
198
202
 
199
203
  Args:
200
204
  process_id:
@@ -210,7 +214,8 @@ class AsyncClient(ClientAppMixin, AsyncClientMixin):
210
214
 
211
215
  - `404`: The requested URI was not found.
212
216
  """
213
- return await self._transport.async_call(
217
+ transport = self._get_transport()
218
+ return await transport.async_call(
214
219
  TransportArgs(
215
220
  path="/processes/{processID}",
216
221
  method="get",
@@ -247,7 +252,8 @@ class AsyncClient(ClientAppMixin, AsyncClientMixin):
247
252
  - `404`: The requested URI was not found.
248
253
  - `500`: A server error occurred.
249
254
  """
250
- return await self._transport.async_call(
255
+ transport = self._get_transport()
256
+ return await transport.async_call(
251
257
  TransportArgs(
252
258
  path="/processes/{processID}/execution",
253
259
  method="post",
@@ -276,7 +282,8 @@ class AsyncClient(ClientAppMixin, AsyncClientMixin):
276
282
 
277
283
  - `404`: The requested URI was not found.
278
284
  """
279
- return await self._transport.async_call(
285
+ transport = self._get_transport()
286
+ return await transport.async_call(
280
287
  TransportArgs(
281
288
  path="/jobs",
282
289
  method="get",
@@ -291,8 +298,8 @@ class AsyncClient(ClientAppMixin, AsyncClientMixin):
291
298
  Show the status of a job.
292
299
 
293
300
  For more information, see [OGC API — Processes — Part 1
294
- Section 7.12](https://docs.ogc.org/is/18-062r2/18-
295
- 062r2.html#sc_retrieve_status_info).
301
+ Section
302
+ 7.12](https://docs.ogc.org/is/18-062r2/18-062r2.html#sc_retrieve_status_info).
296
303
 
297
304
  Args:
298
305
  job_id: Local identifier of a job
@@ -309,7 +316,8 @@ class AsyncClient(ClientAppMixin, AsyncClientMixin):
309
316
  - `404`: The requested URI was not found.
310
317
  - `500`: A server error occurred.
311
318
  """
312
- return await self._transport.async_call(
319
+ transport = self._get_transport()
320
+ return await transport.async_call(
313
321
  TransportArgs(
314
322
  path="/jobs/{jobId}",
315
323
  method="get",
@@ -342,7 +350,8 @@ class AsyncClient(ClientAppMixin, AsyncClientMixin):
342
350
  - `404`: The requested URI was not found.
343
351
  - `500`: A server error occurred.
344
352
  """
345
- return await self._transport.async_call(
353
+ transport = self._get_transport()
354
+ return await transport.async_call(
346
355
  TransportArgs(
347
356
  path="/jobs/{jobId}",
348
357
  method="delete",
@@ -359,8 +368,8 @@ class AsyncClient(ClientAppMixin, AsyncClientMixin):
359
368
  instead.
360
369
 
361
370
  For more information, see [OGC API — Processes — Part 1
362
- Section 7.13](https://docs.ogc.org/is/18-062r2/18-
363
- 062r2.html#sc_retrieve_job_results).
371
+ Section
372
+ 7.13](https://docs.ogc.org/is/18-062r2/18-062r2.html#sc_retrieve_job_results).
364
373
 
365
374
  Args:
366
375
  job_id: Local identifier of a job
@@ -377,7 +386,8 @@ class AsyncClient(ClientAppMixin, AsyncClientMixin):
377
386
  - `404`: The requested URI was not found.
378
387
  - `500`: A server error occurred.
379
388
  """
380
- return await self._transport.async_call(
389
+ transport = self._get_transport()
390
+ return await transport.async_call(
381
391
  TransportArgs(
382
392
  path="/jobs/{jobId}/results",
383
393
  method="get",
@@ -387,8 +397,3 @@ class AsyncClient(ClientAppMixin, AsyncClientMixin):
387
397
  extra_kwargs=kwargs,
388
398
  )
389
399
  )
390
-
391
- async def close(self):
392
- """Close this client."""
393
- if self._transport is not None:
394
- await self._transport.async_close()
@@ -0,0 +1,319 @@
1
+ # Copyright (c) 2026 by the Eozilla team and contributors
2
+ # Permissions are hereby granted under the terms of the Apache 2.0 License:
3
+ # https://opensource.org/license/apache-2-0.
4
+
5
+ import asyncio
6
+ import threading
7
+ import warnings
8
+ from abc import abstractmethod
9
+ from typing import Any
10
+
11
+ import httpx2
12
+ from authlib.integrations.httpx_client import AsyncOAuth2Client
13
+
14
+ from gavicore.models import JobInfo, JobResults, JobStatus, ProcessDescription
15
+ from gavicore.util.request import ExecutionRequest
16
+
17
+ from .auth.config import OidcAuthConfig
18
+ from .auth.interactive import authorize
19
+ from .auth.oidc import LoopbackCallbackServer
20
+ from .client_mixin_base import ClientMixinBase
21
+ from .defaults import (
22
+ DEFAULT_OPEN_JOB_JOB_POLL_INTERVAL,
23
+ DEFAULT_OPEN_JOB_RESULT_TIMEOUT,
24
+ )
25
+ from .exceptions import ClientError, ClientWarning
26
+ from .opener import JobResultOpenContext, JobResultStatusError
27
+ from .opener.opener import open_job_result
28
+ from .transport import AsyncTransport
29
+
30
+ # -----------------------------------------------------
31
+ # IMPORTANT: Sync changes here with ClientMixin!
32
+ # -----------------------------------------------------
33
+
34
+
35
+ # noinspection PyShadowingBuiltins
36
+ class AsyncClientMixin(ClientMixinBase[httpx2.AsyncClient]):
37
+ """
38
+ Extra methods for the API client (asynchronous mode).
39
+ """
40
+
41
+ _async_mode = True
42
+ _transport: AsyncTransport | None
43
+
44
+ def _init_client_runtime(self) -> None:
45
+ super()._init_client_runtime()
46
+ self._runtime_lock = asyncio.Lock()
47
+ self._owner_loop: asyncio.AbstractEventLoop | None = None
48
+
49
+ def _bind_loop(self) -> asyncio.AbstractEventLoop:
50
+ loop = asyncio.get_running_loop()
51
+ if self._owner_loop is not None and self._owner_loop is not loop:
52
+ raise RuntimeError("AsyncClient must be used on its owning event loop.")
53
+ self._owner_loop = loop
54
+ return loop
55
+
56
+ async def close(self) -> None:
57
+ """Close owned connections. A closed client cannot be used again."""
58
+ self._bind_loop()
59
+ async with self._runtime_lock:
60
+ await self._close()
61
+
62
+ async def _close(self) -> None:
63
+ """Close connections while the caller holds the owner lock."""
64
+ self._closed = True
65
+ transport, self._transport = self._transport, None
66
+ try:
67
+ if transport is not None:
68
+ await transport.async_close()
69
+ finally:
70
+ if self._http_client is not None:
71
+ await self._http_client.aclose()
72
+ self._http_client = None
73
+
74
+ async def login(
75
+ self,
76
+ *,
77
+ interactive: bool = True,
78
+ no_browser: bool = False,
79
+ force: bool = False,
80
+ save: bool = False,
81
+ ) -> None:
82
+ """Prepare authentication using the client's persistent HTTP session.
83
+
84
+ Use force to acquire a fresh token. Requests never prompt or open a
85
+ browser. With save=True, failure to save credentials raises an error.
86
+ """
87
+ self._bind_loop()
88
+ async with self._runtime_lock:
89
+ await self._login(
90
+ interactive=interactive, no_browser=no_browser, force=force, save=save
91
+ )
92
+
93
+ async def _login(
94
+ self,
95
+ *,
96
+ interactive: bool = False,
97
+ no_browser: bool = False,
98
+ force: bool = False,
99
+ save: bool = False,
100
+ ) -> None:
101
+ self._require_open()
102
+ auth = (
103
+ await asyncio.to_thread(self._credentials, interactive=True, force=force)
104
+ if interactive
105
+ else self._credentials(interactive=False, force=force)
106
+ )
107
+ self._configure_http_client(auth, self._updated_token)
108
+ await self._discover()
109
+ client = self._http_client
110
+ response = None
111
+ if isinstance(client, AsyncOAuth2Client) and client.token and not force:
112
+ await client.ensure_active_token(client.token)
113
+ elif isinstance(auth, OidcAuthConfig):
114
+ assert isinstance(client, AsyncOAuth2Client)
115
+ with LoopbackCallbackServer() as server:
116
+ url, state, verifier = self._authorization(client, server)
117
+ cancelled = threading.Event()
118
+ try:
119
+ code = await asyncio.to_thread(
120
+ authorize,
121
+ server,
122
+ url,
123
+ state,
124
+ no_browser=no_browser,
125
+ cancelled=cancelled,
126
+ )
127
+ except asyncio.CancelledError:
128
+ cancelled.set()
129
+ raise
130
+ response = await client.fetch_token(code=code, code_verifier=verifier)
131
+ await self._validate_oidc_token(required=True)
132
+ elif request := self._login_request(force=force):
133
+ response = await request()
134
+ self._accept_login(response, force=force, save=save)
135
+
136
+ async def _discover(self) -> None:
137
+ if url := self._discovery_url:
138
+ assert self._http_client is not None
139
+ self._accept_discovery(
140
+ await self._http_client.request("GET", url, auth=None)
141
+ )
142
+
143
+ async def _validate_oidc_token(self, *, required: bool = False) -> None:
144
+ with self._oidc_validation(required=required) as url:
145
+ if url:
146
+ assert self._http_client is not None
147
+ response = await self._http_client.request("GET", url, auth=None)
148
+ self._validate_id_token(response, required=required)
149
+
150
+ async def _updated_token(self, _token: dict[str, Any], **_previous: Any) -> None:
151
+ await self._validate_oidc_token()
152
+ self._save_credentials()
153
+
154
+ async def logout(self) -> None:
155
+ """Revoke an OIDC token when supported, remove local secrets, and close."""
156
+ self._bind_loop()
157
+ async with self._runtime_lock:
158
+ self._require_open()
159
+ try:
160
+ if self._can_revoke:
161
+ self._configure_http_client(self.config.auth, self._updated_token)
162
+ await self._discover()
163
+ if options := self._revocation():
164
+ assert isinstance(self._http_client, AsyncOAuth2Client)
165
+ response = await self._http_client.revoke_token(**options)
166
+ response.raise_for_status()
167
+ finally:
168
+ self._closed = True
169
+ try:
170
+ self._forget_credentials()
171
+ finally:
172
+ await self._close()
173
+
174
+ async def _request(self, method: str, url: str, **kwargs: Any) -> httpx2.Response:
175
+ self._bind_loop()
176
+ async with self._runtime_lock:
177
+ await self._login()
178
+ assert self._http_client is not None
179
+ return await self._http_client.request(
180
+ method, url, **self._request_options(kwargs)
181
+ )
182
+
183
+ def _app_callbacks(self) -> dict[str, Any]:
184
+ self._require_open()
185
+ loop = self._bind_loop()
186
+
187
+ def require_running_owner() -> None:
188
+ self._require_open()
189
+ if not loop.is_running():
190
+ raise RuntimeError("The AsyncClient owning event loop is not running.")
191
+
192
+ async def prepare() -> None:
193
+ require_running_owner()
194
+ await asyncio.wrap_future(
195
+ asyncio.run_coroutine_threadsafe(self.login(interactive=False), loop)
196
+ )
197
+
198
+ async def request(method: str, url: str, **kwargs: Any) -> httpx2.Response:
199
+ require_running_owner()
200
+ return await asyncio.wrap_future(
201
+ asyncio.run_coroutine_threadsafe(
202
+ self._request(method, url, **kwargs), loop
203
+ )
204
+ )
205
+
206
+ return dict(prepare=prepare, request=request)
207
+
208
+ @abstractmethod
209
+ async def get_process(self, process_id: str, **kwargs: Any) -> ProcessDescription:
210
+ """Will be overridden by the actual client class."""
211
+
212
+ @abstractmethod
213
+ async def get_job(self, job_id: str, **kwargs: Any) -> JobInfo:
214
+ """Will be overridden by the actual client class."""
215
+
216
+ @abstractmethod
217
+ async def get_job_results(self, job_id: str, **kwargs: Any) -> JobResults:
218
+ """Will be overridden by the actual client class."""
219
+
220
+ async def create_execution_request(
221
+ self,
222
+ process_id: str,
223
+ dotpath: bool = False,
224
+ ) -> ExecutionRequest:
225
+ """
226
+ Create a template for an execution request
227
+ generated from the process description of the
228
+ given process identifier.
229
+
230
+ Args:
231
+ process_id: The process identifier
232
+ dotpath: Whether to create dot-separated input
233
+ names for nested object values
234
+
235
+ Returns:
236
+ The execution request template.
237
+
238
+ Raises:
239
+ ClientError: if an error occurs
240
+ """
241
+ process_description = await self.get_process(process_id)
242
+ return ExecutionRequest.from_process_description(
243
+ process_description, dotpath=dotpath
244
+ )
245
+
246
+ async def open_job_result(
247
+ self,
248
+ job_id: str,
249
+ output_name: str | None = None,
250
+ data_type: type | None = None,
251
+ media_type: str | None = None,
252
+ poll_interval: float = DEFAULT_OPEN_JOB_JOB_POLL_INTERVAL,
253
+ timeout: float = DEFAULT_OPEN_JOB_RESULT_TIMEOUT,
254
+ **options: Any,
255
+ ) -> Any:
256
+ """Open the results of the job given by its ID.
257
+
258
+ Args:
259
+ job_id: the job ID
260
+ output_name: the name of the output to be opened.
261
+ data_type: the expected/desired data type to be returned.
262
+ If provided, the return value will be of that type.
263
+ If not provided, the return value will be the type
264
+ decided by the opener.
265
+ media_type: the media type of the output produced.
266
+ Only needed, if the output does not provide its
267
+ media type or if its media type should be overridden.
268
+ poll_interval: interval in seconds between job status polls.
269
+ Applies while job status is still "accepted" or "running".
270
+ timeout: maximum time in seconds to wait for job completion.
271
+ options: additional opener-specific options.
272
+
273
+ Returns:
274
+ The job result value.
275
+
276
+ Raises:
277
+ ClientError: if an API error occurs
278
+ JobResultOpenError: if an opener error occurs
279
+ JobResultStatusError: if the job failed or was canceled
280
+ TimeoutError: if the job does not finish within the timeout
281
+ """
282
+ deadline = asyncio.get_running_loop().time() + timeout
283
+ while True:
284
+ job_info = await self.get_job(job_id)
285
+ if job_info.status not in (JobStatus.accepted, JobStatus.running):
286
+ break
287
+ if asyncio.get_running_loop().time() >= deadline:
288
+ raise TimeoutError(
289
+ f"Cannot open result of job #{job_id}; "
290
+ f"it did not finish within {timeout} seconds"
291
+ )
292
+ await asyncio.sleep(poll_interval)
293
+ if job_info.status != JobStatus.successful:
294
+ raise JobResultStatusError(job_info)
295
+ job_results = await self.get_job_results(job_id)
296
+ process_id = job_info.processID
297
+ process_description: ProcessDescription | None = None
298
+ if process_id:
299
+ try:
300
+ process_description = await self.get_process(process_id)
301
+ except ClientError:
302
+ warnings.warn(
303
+ "An error occurred while getting process "
304
+ f"description for process ID {process_id!r}",
305
+ category=ClientWarning,
306
+ stacklevel=2,
307
+ )
308
+ ctx = JobResultOpenContext(
309
+ config=self.config,
310
+ job_id=job_id,
311
+ job_results=job_results,
312
+ process_description=process_description,
313
+ output_name=output_name,
314
+ data_type=data_type,
315
+ _media_type=media_type,
316
+ options=options,
317
+ )
318
+ opener_registry = self.config.get_job_result_opener_registry()
319
+ return await open_job_result(ctx, *opener_registry.opener_types)
@@ -0,0 +1,35 @@
1
+ # Copyright (c) 2025-2026 by the Eozilla team and contributors
2
+ # Permissions are hereby granted under the terms of the Apache 2.0 License:
3
+ # https://opensource.org/license/apache-2.0.
4
+
5
+ from .config import (
6
+ ApiKeyAuthConfig,
7
+ AuthConfig,
8
+ AuthConfigBase,
9
+ AuthType,
10
+ BasicAuthConfig,
11
+ LoginAuthConfig,
12
+ NoAuthConfig,
13
+ OAuth2AuthConfig,
14
+ OAuth2GrantType,
15
+ OidcAuthConfig,
16
+ SecretFields,
17
+ TokenAuthConfig,
18
+ )
19
+ from .oauth2_client import LoginRequiredError
20
+
21
+ __all__ = [
22
+ "ApiKeyAuthConfig",
23
+ "AuthConfig",
24
+ "AuthConfigBase",
25
+ "AuthType",
26
+ "BasicAuthConfig",
27
+ "LoginAuthConfig",
28
+ "NoAuthConfig",
29
+ "OAuth2AuthConfig",
30
+ "OAuth2GrantType",
31
+ "OidcAuthConfig",
32
+ "SecretFields",
33
+ "TokenAuthConfig",
34
+ "LoginRequiredError",
35
+ ]