rsconnect-python 1.30.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. rsconnect/__init__.py +13 -0
  2. rsconnect/actions.py +565 -0
  3. rsconnect/actions_content.py +508 -0
  4. rsconnect/actions_environment.py +160 -0
  5. rsconnect/actions_integration.py +118 -0
  6. rsconnect/api.py +2582 -0
  7. rsconnect/bundle.py +2481 -0
  8. rsconnect/certificates.py +39 -0
  9. rsconnect/environment.py +390 -0
  10. rsconnect/environment_node.py +115 -0
  11. rsconnect/environment_r.py +300 -0
  12. rsconnect/exception.py +15 -0
  13. rsconnect/git_metadata.py +180 -0
  14. rsconnect/http_support.py +595 -0
  15. rsconnect/json_web_token.py +178 -0
  16. rsconnect/log.py +253 -0
  17. rsconnect/main.py +5889 -0
  18. rsconnect/metadata.py +879 -0
  19. rsconnect/models.py +835 -0
  20. rsconnect/oauth.py +623 -0
  21. rsconnect/py.typed +0 -0
  22. rsconnect/pyproject.py +283 -0
  23. rsconnect/quickstart/__init__.py +16 -0
  24. rsconnect/quickstart/quickstart.py +486 -0
  25. rsconnect/quickstart/templates/__init__.py +16 -0
  26. rsconnect/quickstart/templates/api/README.md.tmpl +15 -0
  27. rsconnect/quickstart/templates/api/__connect__.py.tmpl +3 -0
  28. rsconnect/quickstart/templates/api/__init__.py.tmpl +1 -0
  29. rsconnect/quickstart/templates/api/__main__.py.tmpl +14 -0
  30. rsconnect/quickstart/templates/api/app.py.tmpl +11 -0
  31. rsconnect/quickstart/templates/api/pyproject.toml.tmpl +13 -0
  32. rsconnect/quickstart/templates/fastapi/README.md.tmpl +15 -0
  33. rsconnect/quickstart/templates/fastapi/__connect__.py.tmpl +3 -0
  34. rsconnect/quickstart/templates/fastapi/__init__.py.tmpl +1 -0
  35. rsconnect/quickstart/templates/fastapi/__main__.py.tmpl +16 -0
  36. rsconnect/quickstart/templates/fastapi/app.py.tmpl +11 -0
  37. rsconnect/quickstart/templates/fastapi/pyproject.toml.tmpl +14 -0
  38. rsconnect/quickstart/templates/notebook/README.md.tmpl +15 -0
  39. rsconnect/quickstart/templates/notebook/notebook.ipynb.tmpl +34 -0
  40. rsconnect/quickstart/templates/notebook/pyproject.toml.tmpl +13 -0
  41. rsconnect/quickstart/templates/quarto/README.md.tmpl +19 -0
  42. rsconnect/quickstart/templates/quarto/pyproject.toml.tmpl +11 -0
  43. rsconnect/quickstart/templates/quarto/report.qmd.tmpl +8 -0
  44. rsconnect/quickstart/templates/shiny/README.md.tmpl +15 -0
  45. rsconnect/quickstart/templates/shiny/app.py.tmpl +3 -0
  46. rsconnect/quickstart/templates/shiny/pyproject.toml.tmpl +13 -0
  47. rsconnect/quickstart/templates/streamlit/README.md.tmpl +15 -0
  48. rsconnect/quickstart/templates/streamlit/app.py.tmpl +3 -0
  49. rsconnect/quickstart/templates/streamlit/pyproject.toml.tmpl +13 -0
  50. rsconnect/quickstart/templates/voila/README.md.tmpl +15 -0
  51. rsconnect/quickstart/templates/voila/pyproject.toml.tmpl +14 -0
  52. rsconnect/shiny_express.py +136 -0
  53. rsconnect/snowflake.py +93 -0
  54. rsconnect/subprocesses/__init__.py +0 -0
  55. rsconnect/subprocesses/inspect_environment.py +362 -0
  56. rsconnect/timeouts.py +89 -0
  57. rsconnect/utils_package.py +261 -0
  58. rsconnect/validation.py +156 -0
  59. rsconnect/version_check.py +154 -0
  60. rsconnect_python-1.30.0.dist-info/METADATA +89 -0
  61. rsconnect_python-1.30.0.dist-info/RECORD +63 -0
  62. rsconnect_python-1.30.0.dist-info/WHEEL +4 -0
  63. rsconnect_python-1.30.0.dist-info/entry_points.txt +3 -0
rsconnect/models.py ADDED
@@ -0,0 +1,835 @@
1
+ """
2
+ Data models
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import fnmatch
8
+ import pathlib
9
+ import re
10
+ import sys
11
+ from typing import Callable, Literal, Optional, cast
12
+
13
+ import click
14
+ import semver
15
+ from click import ParamType
16
+ from click.types import StringParamType
17
+
18
+ # Even though TypedDict is available in Python 3.8, because it's used with NotRequired,
19
+ # they should both come from the same typing module.
20
+ # https://peps.python.org/pep-0655/#usage-in-python-3-11
21
+ if sys.version_info >= (3, 11):
22
+ from typing import TypedDict
23
+ else:
24
+ from typing_extensions import TypedDict
25
+
26
+ _version_search_pattern = r"(^[=><]{0,2})(.*)"
27
+ _content_guid_pattern = r"([^,]*),?(.*)"
28
+
29
+
30
+ class BuildStatus:
31
+ NEEDS_BUILD = "NEEDS_BUILD" # marked for build
32
+ RUNNING = "RUNNING" # running now
33
+ ABORTED = "ABORTED" # cancelled while running
34
+ COMPLETE = "COMPLETE" # completed successfully
35
+ ERROR = "ERROR" # completed with an error
36
+
37
+ _all = [NEEDS_BUILD, RUNNING, ABORTED, COMPLETE, ERROR]
38
+
39
+
40
+ class AppMode:
41
+ """
42
+ Data class defining an "app mode" as understood by Posit
43
+ Connect
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ ordinal: int,
49
+ name: AppModes.Modes,
50
+ text: str,
51
+ ext: Optional[str] = None,
52
+ ):
53
+ self._ordinal = ordinal
54
+ self._name: AppModes.Modes = name
55
+ self._text = text
56
+ self._ext = ext
57
+
58
+ def ordinal(self):
59
+ return self._ordinal
60
+
61
+ def name(self) -> AppModes.Modes:
62
+ return self._name
63
+
64
+ def desc(self):
65
+ return self._text
66
+
67
+ def extension(self):
68
+ return self._ext
69
+
70
+ def cli_alias(self) -> Optional[str]:
71
+ """Return the primary CLI alias for this mode, or ``None`` if absent.
72
+
73
+ "Primary" is the first key declared in :data:`AppModes._cli_aliases`
74
+ that maps to this mode; secondary aliases (e.g. ``flask`` for
75
+ ``PYTHON_API``) are still resolvable via
76
+ :meth:`AppModes.get_by_cli_alias` but are not returned here.
77
+ """
78
+ for alias, mode in AppModes._cli_aliases.items():
79
+ if mode is self:
80
+ return alias
81
+ return None
82
+
83
+ def __str__(self):
84
+ return self.name()
85
+
86
+ def __repr__(self):
87
+ return self.desc()
88
+
89
+
90
+ class AppModes:
91
+ """
92
+ Enumeration-like collection of known `AppMode`s with lookup
93
+ functions
94
+ """
95
+
96
+ UNKNOWN = AppMode(0, "unknown", "<unknown>")
97
+ SHINY = AppMode(1, "shiny", "Shiny App", ".R")
98
+ RMD = AppMode(3, "rmd-static", "R Markdown", ".Rmd")
99
+ SHINY_RMD = AppMode(2, "rmd-shiny", "Shiny App (Rmd)")
100
+ STATIC = AppMode(4, "static", "Static HTML", ".html")
101
+ PLUMBER = AppMode(5, "api", "API")
102
+ TENSORFLOW = AppMode(6, "tensorflow-saved-model", "TensorFlow Model")
103
+ JUPYTER_NOTEBOOK = AppMode(7, "jupyter-static", "Jupyter Notebook", ".ipynb")
104
+ PYTHON_API = AppMode(8, "python-api", "Python API")
105
+ DASH_APP = AppMode(9, "python-dash", "Dash Application")
106
+ STREAMLIT_APP = AppMode(10, "python-streamlit", "Streamlit Application")
107
+ BOKEH_APP = AppMode(11, "python-bokeh", "Bokeh Application")
108
+ PYTHON_FASTAPI = AppMode(12, "python-fastapi", "Python FastAPI")
109
+ SHINY_QUARTO = AppMode(13, "quarto-shiny", "Shiny Quarto Document")
110
+ STATIC_QUARTO = AppMode(14, "quarto-static", "Quarto Document", ".qmd")
111
+ PYTHON_SHINY = AppMode(15, "python-shiny", "Python Shiny Application")
112
+ JUPYTER_VOILA = AppMode(16, "jupyter-voila", "Jupyter Voila Application")
113
+ PYTHON_GRADIO = AppMode(17, "python-gradio", "Gradio Application")
114
+ PYTHON_PANEL = AppMode(18, "python-panel", "Panel Application")
115
+ NODE_JS = AppMode(20, "nodejs", "Node.js application")
116
+
117
+ _modes = [
118
+ UNKNOWN,
119
+ SHINY,
120
+ RMD,
121
+ SHINY_RMD,
122
+ STATIC,
123
+ PLUMBER,
124
+ TENSORFLOW,
125
+ JUPYTER_NOTEBOOK,
126
+ PYTHON_API,
127
+ DASH_APP,
128
+ STREAMLIT_APP,
129
+ BOKEH_APP,
130
+ PYTHON_FASTAPI,
131
+ SHINY_QUARTO,
132
+ STATIC_QUARTO,
133
+ PYTHON_SHINY,
134
+ JUPYTER_VOILA,
135
+ PYTHON_GRADIO,
136
+ PYTHON_PANEL,
137
+ NODE_JS,
138
+ ]
139
+
140
+ Modes = Literal[
141
+ "unknown",
142
+ "shiny",
143
+ "rmd-static",
144
+ "rmd-shiny",
145
+ "static",
146
+ "api",
147
+ "tensorflow-saved-model",
148
+ "jupyter-static",
149
+ "python-api",
150
+ "python-dash",
151
+ "python-streamlit",
152
+ "python-bokeh",
153
+ "python-fastapi",
154
+ "quarto-shiny",
155
+ "quarto-static",
156
+ "python-shiny",
157
+ "jupyter-voila",
158
+ "python-gradio",
159
+ "python-panel",
160
+ "nodejs",
161
+ ]
162
+
163
+ _cloud_to_connect_modes = {
164
+ "shiny": SHINY,
165
+ "rmarkdown_static": RMD,
166
+ "rmarkdown": SHINY_RMD,
167
+ "plumber": PLUMBER,
168
+ "flask": PYTHON_API,
169
+ "dash": DASH_APP,
170
+ "streamlit": STREAMLIT_APP,
171
+ "fastapi": PYTHON_FASTAPI,
172
+ "bokeh": BOKEH_APP,
173
+ }
174
+
175
+ # CLI alias vocabulary used by ``rsconnect deploy <alias>`` and
176
+ # ``rsconnect quickstart <alias>``. Many-to-one is allowed: ``api`` and
177
+ # ``flask`` both resolve to ``PYTHON_API``. NB: ``shiny`` here means
178
+ # ``PYTHON_SHINY``, which differs from cloud-name ``shiny`` (R Shiny in
179
+ # ``_cloud_to_connect_modes``); the two namespaces are independent.
180
+ _cli_aliases: dict[str, AppMode] = {
181
+ "api": PYTHON_API,
182
+ "flask": PYTHON_API,
183
+ "fastapi": PYTHON_FASTAPI,
184
+ "dash": DASH_APP,
185
+ "streamlit": STREAMLIT_APP,
186
+ "bokeh": BOKEH_APP,
187
+ "shiny": PYTHON_SHINY,
188
+ "gradio": PYTHON_GRADIO,
189
+ "panel": PYTHON_PANEL,
190
+ "notebook": JUPYTER_NOTEBOOK,
191
+ "voila": JUPYTER_VOILA,
192
+ "quarto": STATIC_QUARTO,
193
+ "quarto-shiny": SHINY_QUARTO,
194
+ "tensorflow": TENSORFLOW,
195
+ "html": STATIC,
196
+ "nodejs": NODE_JS,
197
+ }
198
+
199
+ @classmethod
200
+ def get_by_ordinal(cls, ordinal: int, return_unknown: bool = False) -> AppMode:
201
+ """Get an AppMode by its associated ordinal (integer)"""
202
+ return cls._find_by(
203
+ lambda mode: mode.ordinal() == ordinal,
204
+ "with ordinal %s" % ordinal,
205
+ return_unknown,
206
+ )
207
+
208
+ # Aliases for app mode names that have been renamed in Connect. Local
209
+ # AppStore metadata may still contain the old name from a prior deploy;
210
+ # this lets resolve() succeed instead of raising on re-deploys.
211
+ _name_aliases = {
212
+ "nodejs-api": "nodejs",
213
+ }
214
+
215
+ @classmethod
216
+ def get_by_name(cls, name: str, return_unknown: bool = False) -> AppMode:
217
+ """Get an AppMode by name"""
218
+ name = cls._name_aliases.get(name, name)
219
+ return cls._find_by(lambda mode: mode.name() == name, "named %s" % name, return_unknown)
220
+
221
+ @classmethod
222
+ def get_by_extension(cls, extension: Optional[str], return_unknown: bool = False) -> AppMode:
223
+ """Get an app mode by its associated extension"""
224
+ # We can't allow a lookup by None since some modes have that for an extension.
225
+ if extension is None:
226
+ if return_unknown:
227
+ return cls.UNKNOWN
228
+ raise ValueError("No app mode with extension %s" % extension)
229
+
230
+ return cls._find_by(
231
+ lambda mode: mode.extension() == extension,
232
+ "with extension: %s" % extension,
233
+ return_unknown,
234
+ )
235
+
236
+ @classmethod
237
+ def get_by_cloud_name(cls, name: str) -> AppMode:
238
+ return cls._cloud_to_connect_modes.get(name, cls.UNKNOWN)
239
+
240
+ @classmethod
241
+ def get_by_cli_alias(cls, alias: str) -> AppMode:
242
+ """Resolve a CLI alias to its canonical :class:`AppMode`.
243
+
244
+ Returns :attr:`UNKNOWN` for aliases not in :data:`_cli_aliases`.
245
+ Subcommands that accept only a subset of modes (e.g. ``rsconnect
246
+ quickstart``) check membership in their own registry after resolving
247
+ the alias here.
248
+ """
249
+ return cls._cli_aliases.get(alias, cls.UNKNOWN)
250
+
251
+ @classmethod
252
+ def cli_aliases(cls) -> tuple[str, ...]:
253
+ """All CLI aliases declared in :data:`_cli_aliases`, in declaration order."""
254
+ return tuple(cls._cli_aliases.keys())
255
+
256
+ @classmethod
257
+ def _find_by(cls, predicate: Callable[[AppMode], bool], message: str, return_unknown: bool) -> AppMode:
258
+ for mode in cls._modes:
259
+ if predicate(mode):
260
+ return mode
261
+ if return_unknown:
262
+ return cls.UNKNOWN
263
+ raise ValueError("No app mode %s" % message)
264
+
265
+
266
+ class GlobMatcher(object):
267
+ """
268
+ A simplified means of matching a path against a glob pattern. The key
269
+ limitation is that we support at most one occurrence of the `**` pattern.
270
+ """
271
+
272
+ def __init__(self, pattern: str):
273
+ pattern = pathlib.PurePath(pattern).as_posix()
274
+ if pattern.endswith("/**/*"):
275
+ # Note: the index used here makes sure the pattern has a trailing
276
+ # slash. We want that.
277
+ self._pattern = pattern[:-4]
278
+ self.matches = self._match_with_starts_with
279
+ else:
280
+ self._pattern_parts: list[str | re.Pattern[str]]
281
+ self._wildcard_index: int | None
282
+ self._pattern_parts, self._wildcard_index = self._to_parts_list(pattern)
283
+ self.matches = self._match_with_list_parts
284
+
285
+ @staticmethod
286
+ def _to_parts_list(pattern: str) -> tuple[list[str | re.Pattern[str]], int | None]:
287
+ """
288
+ Converts a glob expression into a list, with an entry for each directory
289
+ level. Each entry will be either a string, in which case an equality
290
+ check for that directory entry, or a regular expression, in which case
291
+ matching will be used. The string, '**', is special but we don't alter
292
+ it here. We do return its index.
293
+
294
+ :param pattern: the glob pattern to pull apart.
295
+ :return: a list of pattern pieces and the index of the special '**' pattern.
296
+ The index will be None if `**` is never found.
297
+ """
298
+ # Incoming pattern is ALWAYS a Posix-style path.
299
+ parts_start = pattern.split("/")
300
+ parts_result: list[str | re.Pattern[str]] = []
301
+ depth_wildcard_index = None
302
+ for index, name in enumerate(parts_start):
303
+ value = name
304
+ if name == "**":
305
+ if depth_wildcard_index is not None:
306
+ raise ValueError('Only one occurrence of the "**" pattern is allowed.')
307
+ depth_wildcard_index = index
308
+ elif any(ch in name for ch in "*?["):
309
+ value = re.compile(r"\A" + fnmatch.translate(name))
310
+ parts_result.append(value)
311
+
312
+ return parts_result, depth_wildcard_index
313
+
314
+ def _match_with_starts_with(self, path: str | pathlib.PurePath):
315
+ path = pathlib.PurePath(path).as_posix()
316
+ return path.startswith(self._pattern)
317
+
318
+ def _match_with_list_parts(self, path: str | pathlib.PurePath):
319
+ path = pathlib.PurePath(path).as_posix()
320
+ parts = path.split("/")
321
+
322
+ def items_match(i1: int, i2: int):
323
+ if i2 >= len(parts):
324
+ return False
325
+ part1 = self._pattern_parts[i1]
326
+ if isinstance(part1, str):
327
+ return self._pattern_parts[i1] == parts[i2]
328
+ return part1.match(parts[i2]) is not None
329
+
330
+ wildcard_index = len(self._pattern_parts) if self._wildcard_index is None else self._wildcard_index
331
+
332
+ # Top-down...
333
+ for index in range(wildcard_index):
334
+ if not items_match(index, index):
335
+ return False
336
+
337
+ if self._wildcard_index is None:
338
+ return len(self._pattern_parts) == len(parts)
339
+
340
+ # Now, bottom-up...
341
+ pattern_index = len(self._pattern_parts) - 1
342
+ part_index = len(parts) - 1
343
+
344
+ while pattern_index > wildcard_index and part_index >= 0:
345
+ if not items_match(pattern_index, part_index):
346
+ return False
347
+ pattern_index = pattern_index - 1
348
+ part_index = part_index - 1
349
+
350
+ return pattern_index == wildcard_index
351
+
352
+
353
+ class GlobSet(object):
354
+ """
355
+ Matches against a set of `GlobMatcher` patterns
356
+ """
357
+
358
+ def __init__(self, patterns: list[str]):
359
+ self._matchers = [GlobMatcher(pattern) for pattern in patterns]
360
+
361
+ def matches(self, path: str):
362
+ """
363
+ Determines whether the given path is matched by any of our glob
364
+ expressions.
365
+
366
+ :param path: the path to test.
367
+ :return: True, if the given path matches any of our glob patterns.
368
+ """
369
+ return any(matcher.matches(path) for matcher in self._matchers)
370
+
371
+
372
+ # Strip quotes from string arguments that might be passed in by jq
373
+ # without the -r flag
374
+ class StrippedStringParamType(StringParamType):
375
+ name = "StrippedString"
376
+
377
+ def convert(self, value: str, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
378
+ value = super(StrippedStringParamType, self).convert(value, param, ctx)
379
+ return value.strip("\"'")
380
+
381
+
382
+ class ContentGuidWithBundle(object):
383
+ def __init__(self, guid: str, bundle_id: Optional[str] = None):
384
+ self.guid = guid
385
+ self.bundle_id = bundle_id
386
+
387
+ def __repr__(self):
388
+ if self.bundle_id:
389
+ return "%s,%s" % (self.guid, self.bundle_id)
390
+ return self.guid
391
+
392
+
393
+ class ContentGuidWithBundleParamType(StrippedStringParamType):
394
+ name = "ContentGuidWithBundle"
395
+
396
+ def convert( # pyright: ignore[reportIncompatibleMethodOverride]
397
+ self,
398
+ value: str | ContentGuidWithBundle,
399
+ param: Optional[click.Parameter],
400
+ ctx: Optional[click.Context],
401
+ ):
402
+ if isinstance(value, ContentGuidWithBundle):
403
+ return value
404
+ if isinstance(value, str):
405
+ value = super(ContentGuidWithBundleParamType, self).convert(value, param, ctx)
406
+ m = re.match(_content_guid_pattern, value)
407
+ if m is not None:
408
+ guid_with_bundle = ContentGuidWithBundle(m.group(1))
409
+ if len(m.groups()) == 2 and len(m.group(2)) > 0:
410
+ try:
411
+ int(m.group(2))
412
+ except ValueError:
413
+ self.fail("Failed to parse bundle_id. Expected Int, but found: %s" % m.group(2))
414
+ guid_with_bundle.bundle_id = m.group(2)
415
+ return guid_with_bundle
416
+ self.fail("Failed to parse content guid arg %s" % value)
417
+
418
+
419
+ AppRole = Literal["owner", "editor", "viewer", "none"]
420
+
421
+
422
+ # Also known as AppRecord in Connect.
423
+ class ContentItemV0(TypedDict):
424
+ id: int
425
+ guid: str
426
+ access_type: Literal["all", "logged_in", "acl"]
427
+ connection_timeout: int | None
428
+ read_timeout: int | None
429
+ init_timeout: int | None
430
+ idle_timeout: int | None
431
+ max_processes: int | None
432
+ min_processes: int | None
433
+ max_conns_per_process: int | None
434
+ load_factor: float | None
435
+ memory_request: float | None
436
+ memory_limit: int | None
437
+ cpu_request: float | None
438
+ cpu_limit: int | None
439
+ amd_gpu_limit: int | None
440
+ nvidia_gpu_limit: int | None
441
+ url: str
442
+ vanity_url: bool
443
+ name: str
444
+ title: str | None
445
+ bundle_id: int | None
446
+ app_mode: int
447
+ content_category: str
448
+ has_parameters: bool
449
+ created_time: str
450
+ last_deployed_time: str
451
+ build_status: int
452
+ cluster_name: str | None
453
+ image_name: str | None
454
+ default_image_name: str | None
455
+ service_account_name: str | None
456
+ r_version: str | None
457
+ py_version: str | None
458
+ quarto_version: str | None
459
+ r_environment_management: bool | None
460
+ default_r_environment_management: bool | None
461
+ py_environment_management: bool | None
462
+ default_py_environment_management: bool | None
463
+ run_as: str | None
464
+ run_as_current_user: bool
465
+ description: str
466
+ EnvironmentJson: str | None
467
+ app_role: AppRole
468
+ owner_first_name: str
469
+ owner_last_name: str
470
+ owner_username: str
471
+ owner_guid: str
472
+ owner_email: str
473
+ owner_locked: bool
474
+ is_scheduled: bool
475
+ # Not sure how the following 4 fields are structured, so just use object for now.
476
+ git: object | None
477
+ users: object | None
478
+ groups: object | None
479
+ vanities: object | None
480
+
481
+
482
+ # Also known as V1 ContentOutputDTO in Connect (note: this is not V1 experimental).
483
+ class ContentItemV1(TypedDict):
484
+ guid: str
485
+ name: str
486
+ title: str | None
487
+ description: str
488
+ access_type: Literal["all", "logged_in", "acl"]
489
+ connection_timeout: int | None
490
+ read_timeout: int | None
491
+ init_timeout: int | None
492
+ idle_timeout: int | None
493
+ max_processes: int | None
494
+ min_processes: int | None
495
+ max_conns_per_process: int | None
496
+ load_factor: float | None
497
+ memory_request: float | None
498
+ memory_limit: int | None
499
+ cpu_request: float | None
500
+ cpu_limit: float | None
501
+ amd_gpu_limit: int | None
502
+ nvidia_gpu_limit: int | None
503
+ service_account_name: str | None
504
+ default_image_name: str | None
505
+ created_time: str
506
+ last_deployed_time: str
507
+ bundle_id: str | None
508
+ app_mode: AppModes.Modes
509
+ content_category: str
510
+ parameterized: bool
511
+ cluster_name: str | None
512
+ image_name: str | None
513
+ r_version: str | None
514
+ py_version: str | None
515
+ quarto_version: str | None
516
+ r_environment_management: bool | None
517
+ default_r_environment_management: bool | None
518
+ py_environment_management: bool | None
519
+ default_py_environment_management: bool | None
520
+ run_as: str | None
521
+ run_as_current_user: bool
522
+ owner_guid: str
523
+ content_url: str
524
+ dashboard_url: str
525
+ app_role: AppRole
526
+ id: str
527
+
528
+
529
+ VersionProgramName = Literal["r_version", "py_version", "quarto_version"]
530
+ ComparisonOperator = Literal[">", "<", ">=", "<=", "=", "=="]
531
+
532
+
533
+ class VersionSearchFilter(object):
534
+ def __init__(
535
+ self,
536
+ name: VersionProgramName,
537
+ comp: ComparisonOperator,
538
+ vers: str,
539
+ ):
540
+ self.name = name
541
+ self.comp = comp
542
+ self.vers = vers
543
+
544
+ def __repr__(self):
545
+ return "%s %s %s" % (self.name, self.comp, self.vers)
546
+
547
+
548
+ class VersionSearchFilterParamType(ParamType):
549
+ name = "VersionSearchFilter"
550
+
551
+ def __init__(self, key: VersionProgramName):
552
+ """
553
+ :param key: key refers to the left side of the version comparison.
554
+ In this case any interpreter in a content result, one of [py_version, r_version, quarto_version]
555
+ """
556
+ self.key: VersionProgramName = key
557
+
558
+ def convert(
559
+ self,
560
+ value: str | VersionSearchFilter,
561
+ param: Optional[click.Parameter],
562
+ ctx: Optional[click.Context],
563
+ ):
564
+ if isinstance(value, VersionSearchFilter):
565
+ return value
566
+
567
+ if isinstance(value, str):
568
+ m = re.match(_version_search_pattern, value)
569
+ if m is not None and len(m.groups()) == 2:
570
+ version_search = VersionSearchFilter(
571
+ name=self.key,
572
+ comp=cast(ComparisonOperator, m.group(1)),
573
+ vers=m.group(2),
574
+ )
575
+
576
+ # default to == if no comparator was provided
577
+ if not version_search.comp:
578
+ version_search.comp = "=="
579
+
580
+ if version_search.comp not in [">", "<", ">=", "<=", "=", "=="]:
581
+ self.fail("Failed to parse verison filter: %s is not a valid comparitor" % version_search.comp)
582
+
583
+ try:
584
+ semver.parse(version_search.vers) # pyright: ignore[reportUnknownMemberType]
585
+ except ValueError:
586
+ self.fail("Failed to parse version info: %s" % version_search.vers)
587
+ return version_search
588
+
589
+ self.fail("Failed to parse version filter %s" % value)
590
+
591
+
592
+ class TaskStatusResult(TypedDict):
593
+ type: str
594
+ data: object # Don't know the structure of this type yet
595
+
596
+
597
+ # https://docs.posit.co/connect/api/#get-/v1/tasks/-id-
598
+ class TaskStatusV1(TypedDict):
599
+ id: str
600
+ output: list[str]
601
+ finished: bool
602
+ code: int
603
+ error: str
604
+ last: int
605
+ result: TaskStatusResult | None
606
+
607
+ # redundant fields for compatibility with rsconnect-python.
608
+ last_status: int
609
+ status: list[str]
610
+
611
+
612
+ class BootstrapOutputDTO(TypedDict):
613
+ api_key: str
614
+
615
+
616
+ # This not the complete specification of the server settings data structure, but it is
617
+ # sufficient for the purposes of this package.
618
+ class ServerSettings(TypedDict):
619
+ hostname: str
620
+ version: str
621
+
622
+
623
+ class PyInfo(TypedDict):
624
+ installations: list[PyInstallation]
625
+ api_enabled: bool
626
+
627
+
628
+ class PyInstallation(TypedDict):
629
+ version: str
630
+ cluster_name: str
631
+ image_name: str
632
+
633
+
634
+ class BuildOutputDTO(TypedDict):
635
+ task_id: str
636
+
637
+
638
+ class BundleMetadata(TypedDict):
639
+ id: str
640
+
641
+
642
+ class ListEntryOutputDTO(TypedDict):
643
+ language: str
644
+ version: str
645
+ image_name: str
646
+
647
+
648
+ class DeleteInputDTO(TypedDict):
649
+ language: str
650
+ version: str
651
+ image_name: str
652
+ dry_run: bool
653
+
654
+
655
+ class DeleteOutputDTO(TypedDict):
656
+ language: str
657
+ version: str
658
+ iamge_name: str
659
+ task_id: str | None
660
+
661
+
662
+ class UserRecord(TypedDict):
663
+ email: str
664
+ username: str
665
+ first_name: str
666
+ last_name: str
667
+ password: str
668
+ user_role: str
669
+ created_time: str
670
+ updated_time: str
671
+ active_time: str | None
672
+ confirmed: bool
673
+ locked: bool
674
+ guid: str
675
+ preferences: dict[str, object]
676
+ privileges: list[str]
677
+
678
+
679
+ class EnvironmentInstallation(TypedDict):
680
+ version: str
681
+ path: str
682
+
683
+
684
+ class EnvironmentInstallations(TypedDict):
685
+ installations: list[EnvironmentInstallation]
686
+
687
+
688
+ class EnvironmentVolumeSource(TypedDict, total=False):
689
+ volume_type: str
690
+ nfs_host: str | None
691
+ nfs_export_path: str | None
692
+ pvc_name: str | None
693
+
694
+
695
+ class EnvironmentVolumeTarget(TypedDict):
696
+ path: str
697
+ read_only: bool | None
698
+
699
+
700
+ class EnvironmentVolumeMount(TypedDict):
701
+ source: EnvironmentVolumeSource
702
+ target: EnvironmentVolumeTarget
703
+
704
+
705
+ class EnvironmentV1(TypedDict):
706
+ id: str
707
+ guid: str
708
+ created_time: str
709
+ updated_time: str
710
+ title: str | None
711
+ description: str | None
712
+ cluster_name: str
713
+ name: str
714
+ environment_type: str
715
+ matching: str
716
+ supervisor: str | None
717
+ managed_by: str | None
718
+ python: EnvironmentInstallations
719
+ quarto: EnvironmentInstallations
720
+ r: EnvironmentInstallations
721
+ tensorflow: EnvironmentInstallations
722
+ volume_mounts: list[EnvironmentVolumeMount]
723
+
724
+
725
+ class EnvironmentCreateInput(TypedDict, total=False):
726
+ title: str | None
727
+ description: str | None
728
+ cluster_name: str
729
+ name: str
730
+ matching: str | None
731
+ supervisor: str | None
732
+ python: EnvironmentInstallations
733
+ quarto: EnvironmentInstallations
734
+ r: EnvironmentInstallations
735
+ tensorflow: EnvironmentInstallations
736
+ volume_mounts: list[EnvironmentVolumeMount]
737
+
738
+
739
+ class EnvironmentUpdateInput(TypedDict, total=False):
740
+ title: str | None
741
+ description: str | None
742
+ matching: str | None
743
+ supervisor: str | None
744
+ python: EnvironmentInstallations
745
+ quarto: EnvironmentInstallations
746
+ r: EnvironmentInstallations
747
+ tensorflow: EnvironmentInstallations
748
+ volume_mounts: list[EnvironmentVolumeMount]
749
+
750
+
751
+ class EnvironmentPermissionV1(TypedDict):
752
+ id: str
753
+ guid: str
754
+ environment_guid: str
755
+ user_guid: str | None
756
+ group_guid: str | None
757
+
758
+
759
+ class EnvironmentPermissionInput(TypedDict, total=False):
760
+ user_guid: str | None
761
+ group_guid: str | None
762
+
763
+
764
+ class OAuthIntegrationPermission(TypedDict):
765
+ user_guid: str | None
766
+ group_guid: str | None
767
+
768
+
769
+ class OAuthIntegration(TypedDict):
770
+ guid: str
771
+ name: str | None
772
+ description: str | None
773
+ template: str | None
774
+ auth_type: str | None
775
+ config: dict[str, object]
776
+ permissions: list[OAuthIntegrationPermission]
777
+ environment_variables: list[str]
778
+ created_time: str
779
+ updated_time: str
780
+
781
+
782
+ class OAuthIntegrationInput(TypedDict, total=False):
783
+ name: str | None
784
+ description: str | None
785
+ template: str
786
+ config: dict[str, object]
787
+ permissions: list[OAuthIntegrationPermission] | None
788
+
789
+
790
+ class OAuthIntegrationUpdate(TypedDict, total=False):
791
+ name: str | None
792
+ description: str | None
793
+ config: dict[str, object]
794
+ permissions: list[OAuthIntegrationPermission] | None
795
+
796
+
797
+ class OAuthTemplate(TypedDict):
798
+ id: str
799
+ name: str
800
+ description: str
801
+ fields: list[object]
802
+ options: list[object]
803
+
804
+
805
+ class KeyValueParamType(ParamType):
806
+ name = "key=value"
807
+
808
+ def convert(
809
+ self,
810
+ value: str | tuple[str, str],
811
+ param: Optional[click.Parameter],
812
+ ctx: Optional[click.Context],
813
+ ) -> tuple[str, str]:
814
+ if isinstance(value, tuple):
815
+ return value
816
+ try:
817
+ k, v = value.split("=", 1)
818
+ return (k.strip(), v.strip())
819
+ except ValueError:
820
+ self.fail(f"'{value}' is not in 'key=value' format", param, ctx)
821
+
822
+
823
+ class RepositoryInfo(TypedDict):
824
+ repository: str
825
+ branch: str
826
+ directory: str
827
+ polling: bool
828
+ last_error: str
829
+ last_known_commit: str
830
+
831
+
832
+ class RepositoryBundleOutput(TypedDict):
833
+ bundle_id: str
834
+ task_id: str
835
+ location: dict[str, str]