deepflow-opendesk-cli 0.1.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.
@@ -0,0 +1,384 @@
1
+ """OpenAPI schema parsing and admin-configuration endpoint filtering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import fnmatch
6
+ import hashlib
7
+ import json
8
+ import re
9
+ from dataclasses import asdict, dataclass, field
10
+ from pathlib import Path
11
+ from typing import Any
12
+ from urllib.error import HTTPError, URLError
13
+ from urllib.request import Request, urlopen
14
+
15
+
16
+ CONFIG_TAGS = {
17
+ "System",
18
+ "SystemSettings",
19
+ "OpenAgentSettings",
20
+ "Roles",
21
+ "Employees",
22
+ "EmployeeGroups",
23
+ "EmployeeSelect",
24
+ "ServiceHours",
25
+ "Channels",
26
+ "ConversationSettings",
27
+ "FieldDefinitions",
28
+ "FormLayouts",
29
+ "InteractionRules",
30
+ "UserViews",
31
+ "TicketViews",
32
+ "OrganizationViews",
33
+ "VoiceFlows",
34
+ "TicketWorkflows",
35
+ "InboundRoutingRules",
36
+ "SessionRoutingRules",
37
+ "Tenants",
38
+ "SIP Trunks",
39
+ "Phone Numbers",
40
+ }
41
+
42
+ INCLUDE_PATHS = (
43
+ "/api/v1/auth/login",
44
+ "/api/v1/auth/me",
45
+ "/api/v1/session-summary/config*",
46
+ "/api/v1/call-summary/config*",
47
+ "/api/v1/queue/policies*",
48
+ "/api/v1/upload/avatar",
49
+ "/api/v1/upload/channel-logo",
50
+ "/api/v1/upload/channel-favicon",
51
+ "/api/v1/call-center/phone-numbers*",
52
+ )
53
+
54
+ EXCLUDE_PATHS = (
55
+ "/api/v1/auth/reset-password",
56
+ "/api/v1/auth/send-verify-code",
57
+ "/api/v1/public/*",
58
+ "/api/v1/conversations*",
59
+ "/api/v1/session-records*",
60
+ "/api/v1/tickets*",
61
+ "/api/v1/users*",
62
+ "/api/v1/organizations*",
63
+ "/api/v1/reports*",
64
+ "/api/v1/workspace/*",
65
+ "/api/v1/agent/*",
66
+ "/api/v1/call-center/agents/*",
67
+ "/api/v1/queue/tasks*",
68
+ "/api/v1/queue/dispatch",
69
+ "/api/v1/queue/state",
70
+ "/api/v1/upload/custom-field-file",
71
+ )
72
+
73
+ CURATED_ALIASES: dict[tuple[str, str], str] = {
74
+ ("POST", "/api/v1/auth/login"): "auth.login",
75
+ ("GET", "/api/v1/auth/me"): "auth.me",
76
+ ("GET", "/api/v1/system/info"): "system.info",
77
+ ("GET", "/api/v1/system-settings"): "system.settings",
78
+ ("PUT", "/api/v1/system-settings"): "system.settings_update",
79
+ ("PUT", "/api/v1/system-settings/organization"): "system.organization_update",
80
+ ("GET", "/api/v1/roles"): "roles.list",
81
+ ("POST", "/api/v1/roles"): "roles.create",
82
+ ("GET", "/api/v1/roles/permission-tree"): "roles.permission_tree",
83
+ ("GET", "/api/v1/employees"): "employees.list",
84
+ ("POST", "/api/v1/employees"): "employees.create",
85
+ ("GET", "/api/v1/employee-groups"): "employee_groups.list",
86
+ ("GET", "/api/v1/field-definitions"): "fields.list",
87
+ ("POST", "/api/v1/field-definitions"): "fields.create",
88
+ ("GET", "/api/v1/field-definitions/unified"): "fields.unified",
89
+ ("GET", "/api/v1/form-layouts/by-scene/{scene}"): "forms.get_by_scene",
90
+ ("GET", "/api/v1/channels"): "channels.list",
91
+ ("GET", "/api/v1/channels/{channel_id}"): "channels.get",
92
+ ("GET", "/api/v1/session-routing-rules"): "routing.session_list",
93
+ ("GET", "/api/v1/inbound-routing-rules"): "routing.inbound_list",
94
+ ("GET", "/api/v1/queue/policies"): "queue.policies",
95
+ ("GET", "/api/v1/voice-flows"): "voice_flows.list",
96
+ ("GET", "/api/v1/ticket-workflows"): "ticket_workflows.list",
97
+ }
98
+
99
+ HTTP_METHODS = {"GET", "POST", "PUT", "PATCH", "DELETE"}
100
+ NO_AUTH_PATHS = {"/api/v1/auth/login", "/api/v1/system/info", "/api/v1/health"}
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class ParamDoc:
105
+ """Path or query parameter documentation."""
106
+
107
+ name: str
108
+ type: str
109
+ required: bool
110
+ description: str = ""
111
+
112
+
113
+ @dataclass(frozen=True)
114
+ class EndpointDoc:
115
+ """Machine-readable endpoint documentation for humans and AI agents."""
116
+
117
+ key: str
118
+ operation_id: str
119
+ method: str
120
+ path: str
121
+ tag: str
122
+ summary: str
123
+ path_params: list[ParamDoc] = field(default_factory=list)
124
+ query_params: list[ParamDoc] = field(default_factory=list)
125
+ body_schema: str = ""
126
+ response_schema: str = ""
127
+ requires_auth: bool = True
128
+ examples: list[str] = field(default_factory=list)
129
+
130
+ def to_dict(self) -> dict[str, Any]:
131
+ """Return a JSON-serializable representation."""
132
+ return asdict(self)
133
+
134
+
135
+ def read_schema_file(path: str | Path) -> dict[str, Any]:
136
+ """Read an OpenAPI schema object from disk."""
137
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
138
+ if not isinstance(data, dict):
139
+ raise ValueError("OpenAPI schema must be a JSON object")
140
+ return data
141
+
142
+
143
+ def fetch_openapi_schema(host: str, token: str | None = None, timeout: int = 30) -> dict[str, Any]:
144
+ """Fetch ``/openapi.json`` from an OpenDesk host."""
145
+ url = f"{host.rstrip('/')}/openapi.json"
146
+ headers = {"Accept": "application/json"}
147
+ if token:
148
+ headers["Authorization"] = f"Bearer {token}"
149
+ request = Request(url, headers=headers, method="GET")
150
+ try:
151
+ with urlopen(request, timeout=timeout) as response: # noqa: S310 - CLI calls user-provided hosts.
152
+ payload = response.read().decode("utf-8")
153
+ except HTTPError as exc:
154
+ body = exc.read().decode("utf-8", errors="replace")
155
+ raise RuntimeError(f"HTTP {exc.code} while fetching OpenAPI schema: {body}") from exc
156
+ except URLError as exc:
157
+ raise RuntimeError(f"Failed to fetch OpenAPI schema from {url}: {exc.reason}") from exc
158
+ data = json.loads(payload)
159
+ if not isinstance(data, dict):
160
+ raise RuntimeError("OpenAPI schema response must be a JSON object")
161
+ return data
162
+
163
+
164
+ def build_catalog(schema: dict[str, Any]) -> list[EndpointDoc]:
165
+ """Build the configuration endpoint catalog from an OpenAPI schema."""
166
+ endpoints: list[EndpointDoc] = []
167
+ paths = schema.get("paths", {})
168
+ if not isinstance(paths, dict):
169
+ return endpoints
170
+
171
+ for path, operations in paths.items():
172
+ if not isinstance(path, str) or not isinstance(operations, dict):
173
+ continue
174
+ for raw_method, operation in operations.items():
175
+ method = raw_method.upper()
176
+ if method not in HTTP_METHODS or not isinstance(operation, dict):
177
+ continue
178
+ tag = _first_tag(operation)
179
+ if not is_config_endpoint(method, path, tag):
180
+ continue
181
+ endpoints.append(_endpoint_from_operation(method, path, tag, operation))
182
+
183
+ return _dedupe_keys(endpoints)
184
+
185
+
186
+ def catalog_by_key(catalog: list[EndpointDoc]) -> dict[str, EndpointDoc]:
187
+ """Index endpoints by key and OpenAPI operation id."""
188
+ indexed: dict[str, EndpointDoc] = {}
189
+ for endpoint in catalog:
190
+ indexed[endpoint.key] = endpoint
191
+ if endpoint.operation_id:
192
+ indexed[endpoint.operation_id] = endpoint
193
+ return indexed
194
+
195
+
196
+ def get_endpoint(catalog: list[EndpointDoc], key: str) -> EndpointDoc:
197
+ """Return an endpoint by key or operation id."""
198
+ indexed = catalog_by_key(catalog)
199
+ try:
200
+ return indexed[key]
201
+ except KeyError as exc:
202
+ choices = ", ".join(sorted(item.key for item in catalog))
203
+ raise KeyError(f"Unknown endpoint '{key}'. Available endpoints: {choices}") from exc
204
+
205
+
206
+ def is_config_endpoint(method: str, path: str, tag: str) -> bool:
207
+ """Return whether an endpoint belongs to the admin configuration surface."""
208
+ _ = method
209
+ if _matches_any(path, EXCLUDE_PATHS):
210
+ return False
211
+ if _matches_any(path, INCLUDE_PATHS):
212
+ return True
213
+ return tag in CONFIG_TAGS
214
+
215
+
216
+ def find_matching_endpoint(catalog: list[EndpointDoc], method: str, path: str) -> EndpointDoc | None:
217
+ """Find a catalog endpoint for a concrete path."""
218
+ method = method.upper()
219
+ for endpoint in catalog:
220
+ if endpoint.method != method:
221
+ continue
222
+ if endpoint.path == path or _path_template_matches(endpoint.path, path):
223
+ return endpoint
224
+ return None
225
+
226
+
227
+ def normalize_identifier(value: str) -> str:
228
+ """Normalize a label into a stable snake_case identifier."""
229
+ value = re.sub(r"[^0-9A-Za-z]+", "_", value).strip("_").lower()
230
+ return re.sub(r"_+", "_", value) or "endpoint"
231
+
232
+
233
+ def schema_ref_name(schema_fragment: Any) -> str:
234
+ """Summarize a schema fragment as a compact string."""
235
+ if not isinstance(schema_fragment, dict):
236
+ return ""
237
+ ref = schema_fragment.get("$ref")
238
+ if isinstance(ref, str):
239
+ return ref.rsplit("/", 1)[-1]
240
+ if "type" in schema_fragment:
241
+ return str(schema_fragment["type"])
242
+ if "allOf" in schema_fragment:
243
+ return "allOf"
244
+ if "anyOf" in schema_fragment:
245
+ return "anyOf"
246
+ if "oneOf" in schema_fragment:
247
+ return "oneOf"
248
+ return ""
249
+
250
+
251
+ def _endpoint_from_operation(method: str, path: str, tag: str, operation: dict[str, Any]) -> EndpointDoc:
252
+ operation_id = str(operation.get("operationId") or "")
253
+ summary = str(operation.get("summary") or operation_id or f"{method} {path}")
254
+ key = CURATED_ALIASES.get((method, path)) or f"{normalize_identifier(tag)}.{_operation_action(operation_id, summary)}"
255
+ path_params, query_params = _params(operation)
256
+ body_schema = _request_body_schema(operation)
257
+ response_schema = _response_schema(operation)
258
+ return EndpointDoc(
259
+ key=key,
260
+ operation_id=operation_id,
261
+ method=method,
262
+ path=path,
263
+ tag=tag,
264
+ summary=summary,
265
+ path_params=path_params,
266
+ query_params=query_params,
267
+ body_schema=body_schema,
268
+ response_schema=response_schema,
269
+ requires_auth=path not in NO_AUTH_PATHS,
270
+ examples=[_example_for(key, method, path)],
271
+ )
272
+
273
+
274
+ def _dedupe_keys(endpoints: list[EndpointDoc]) -> list[EndpointDoc]:
275
+ seen: dict[str, int] = {}
276
+ result: list[EndpointDoc] = []
277
+ for endpoint in endpoints:
278
+ count = seen.get(endpoint.key, 0)
279
+ seen[endpoint.key] = count + 1
280
+ if count == 0:
281
+ result.append(endpoint)
282
+ continue
283
+ suffix = hashlib.sha1(f"{endpoint.method} {endpoint.path}".encode("utf-8")).hexdigest()[:8]
284
+ result.append(
285
+ EndpointDoc(
286
+ key=f"{endpoint.key}_{suffix}",
287
+ operation_id=endpoint.operation_id,
288
+ method=endpoint.method,
289
+ path=endpoint.path,
290
+ tag=endpoint.tag,
291
+ summary=endpoint.summary,
292
+ path_params=endpoint.path_params,
293
+ query_params=endpoint.query_params,
294
+ body_schema=endpoint.body_schema,
295
+ response_schema=endpoint.response_schema,
296
+ requires_auth=endpoint.requires_auth,
297
+ examples=endpoint.examples,
298
+ )
299
+ )
300
+ return result
301
+
302
+
303
+ def _first_tag(operation: dict[str, Any]) -> str:
304
+ tags = operation.get("tags")
305
+ if isinstance(tags, list) and tags:
306
+ return str(tags[0])
307
+ return ""
308
+
309
+
310
+ def _params(operation: dict[str, Any]) -> tuple[list[ParamDoc], list[ParamDoc]]:
311
+ path_params: list[ParamDoc] = []
312
+ query_params: list[ParamDoc] = []
313
+ raw_params = operation.get("parameters", [])
314
+ if not isinstance(raw_params, list):
315
+ return path_params, query_params
316
+ for raw in raw_params:
317
+ if not isinstance(raw, dict):
318
+ continue
319
+ schema = raw.get("schema") if isinstance(raw.get("schema"), dict) else {}
320
+ item = ParamDoc(
321
+ name=str(raw.get("name") or ""),
322
+ type=str(schema.get("type") or schema_ref_name(schema) or "string"),
323
+ required=bool(raw.get("required")),
324
+ description=str(raw.get("description") or ""),
325
+ )
326
+ location = raw.get("in")
327
+ if location == "path":
328
+ path_params.append(item)
329
+ elif location == "query":
330
+ query_params.append(item)
331
+ return path_params, query_params
332
+
333
+
334
+ def _request_body_schema(operation: dict[str, Any]) -> str:
335
+ body = operation.get("requestBody")
336
+ if not isinstance(body, dict):
337
+ return ""
338
+ content = body.get("content")
339
+ if not isinstance(content, dict):
340
+ return ""
341
+ for content_type in ("application/json", "multipart/form-data"):
342
+ media = content.get(content_type)
343
+ if isinstance(media, dict):
344
+ return schema_ref_name(media.get("schema"))
345
+ return ""
346
+
347
+
348
+ def _response_schema(operation: dict[str, Any]) -> str:
349
+ responses = operation.get("responses")
350
+ if not isinstance(responses, dict):
351
+ return ""
352
+ response = responses.get("200") or responses.get("201") or responses.get("default")
353
+ if not isinstance(response, dict):
354
+ return ""
355
+ content = response.get("content")
356
+ if not isinstance(content, dict):
357
+ return ""
358
+ media = content.get("application/json")
359
+ if isinstance(media, dict):
360
+ return schema_ref_name(media.get("schema"))
361
+ return ""
362
+
363
+
364
+ def _operation_action(operation_id: str, summary: str) -> str:
365
+ base = operation_id.split("_api_v1_", 1)[0] if operation_id else summary
366
+ return normalize_identifier(base)
367
+
368
+
369
+ def _example_for(key: str, method: str, path: str) -> str:
370
+ if "{" in path:
371
+ return f"deepflow_opendesk_cli call {key} --path name=value"
372
+ if method in {"POST", "PUT", "PATCH"}:
373
+ return f"deepflow_opendesk_cli call {key} --json '{{}}'"
374
+ return f"deepflow_opendesk_cli call {key}"
375
+
376
+
377
+ def _matches_any(path: str, patterns: tuple[str, ...]) -> bool:
378
+ return any(fnmatch.fnmatchcase(path, pattern) for pattern in patterns)
379
+
380
+
381
+ def _path_template_matches(template: str, path: str) -> bool:
382
+ pattern = re.escape(template)
383
+ pattern = re.sub(r"\\\{[^/]+\\\}", r"[^/]+", pattern)
384
+ return re.fullmatch(pattern, path) is not None
@@ -0,0 +1,37 @@
1
+ """Simple workflow plan helpers for OpenDesk admin configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .client import CliError
10
+
11
+ WORKFLOWS: dict[str, str] = {
12
+ "setup-tenant": "Initialize tenant-level system settings and optional tenant metadata.",
13
+ "setup-web-channel": "Configure Web SDK channel, welcome rules, and routing prerequisites.",
14
+ "bootstrap-admin-config": "Apply fields, views, forms, service hours, and routing configuration.",
15
+ }
16
+
17
+
18
+ def list_workflows() -> list[dict[str, str]]:
19
+ """Return available workflow names and descriptions."""
20
+ return [{"name": name, "description": description} for name, description in sorted(WORKFLOWS.items())]
21
+
22
+
23
+ def load_workflow_operations(name: str, json_file: str | None) -> list[dict[str, Any]]:
24
+ """Load workflow operations from a JSON file."""
25
+ if name not in WORKFLOWS:
26
+ choices = ", ".join(sorted(WORKFLOWS))
27
+ raise CliError(f"Unknown workflow '{name}'. Available workflows: {choices}")
28
+ if not json_file:
29
+ raise CliError("workflow run requires --json-file with an operations plan")
30
+ payload = json.loads(Path(json_file).read_text(encoding="utf-8"))
31
+ if isinstance(payload, dict):
32
+ operations = payload.get("operations")
33
+ else:
34
+ operations = payload
35
+ if not isinstance(operations, list) or not all(isinstance(item, dict) for item in operations):
36
+ raise CliError("Workflow plan must be a JSON object with operations[] or a JSON array")
37
+ return list(operations)
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepflow-opendesk-cli
3
+ Version: 0.1.0
4
+ Summary: Command-line client for OpenDesk admin configuration APIs.
5
+ Author: DeepFlow AI
6
+ Project-URL: Repository, https://github.com/DeepFlowAI/opendesk-dev
7
+ Keywords: deepflow,opendesk,cli,openapi,admin,configuration
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+ Provides-Extra: dev
18
+ Requires-Dist: build>=1.0; extra == "dev"
19
+ Requires-Dist: pytest>=8.0; extra == "dev"
20
+ Requires-Dist: twine>=5.0; extra == "dev"
21
+ Dynamic: requires-python
22
+
23
+ # deepflow-opendesk-cli
24
+
25
+ Python command-line client for OpenDesk admin configuration APIs.
26
+
27
+ The CLI is designed for implementation engineers, operators, and AI agents that need to configure OpenDesk through the same APIs used by the admin console.
28
+
29
+ ## Scope
30
+
31
+ This package intentionally exposes only admin configuration APIs. Runtime workspace data, public visitor APIs, reports, and call-center agent actions are excluded by a schema whitelist.
32
+
33
+ Included examples:
34
+
35
+ - System settings
36
+ - Roles, employees, employee groups
37
+ - Service hours
38
+ - Channels
39
+ - Field definitions, form layouts, interaction rules
40
+ - User, ticket, and organization view configuration
41
+ - Satisfaction and welcome-message settings
42
+ - Voice flows, ticket workflows, inbound/session routing rules
43
+ - Queue policies
44
+ - Optional tenant, SIP trunk, and phone-number configuration endpoints when extensions are loaded
45
+
46
+ ## Install For Development
47
+
48
+ ```bash
49
+ cd packages/deepflow-opendesk-cli
50
+ pip install -e ".[dev]"
51
+ ```
52
+
53
+ ## Configure Host And Token
54
+
55
+ ```bash
56
+ export OPENDESK_HOST="http://localhost:5001"
57
+ export OPENDESK_TOKEN="..."
58
+ ```
59
+
60
+ Or log in and save a local profile:
61
+
62
+ ```bash
63
+ deepflow_opendesk_cli auth login \
64
+ --tenant 12058577c8114513 \
65
+ --username test \
66
+ --save-token
67
+ ```
68
+
69
+ Profile files are stored under `~/.opendesk/cli/{profile}/` with owner-only permissions where supported.
70
+
71
+ ## Common Commands
72
+
73
+ ```bash
74
+ deepflow_opendesk_cli schema fetch
75
+ deepflow_opendesk_cli docs list --tag FieldDefinitions
76
+ deepflow_opendesk_cli docs show fields.create --json
77
+
78
+ deepflow_opendesk_cli request GET /api/v1/system/info
79
+ deepflow_opendesk_cli call fields.list --query domain=ticket
80
+ deepflow_opendesk_cli call fields.create --json-file field.json
81
+
82
+ deepflow_opendesk_cli system settings
83
+ deepflow_opendesk_cli roles permission-tree
84
+ deepflow_opendesk_cli employees list
85
+ deepflow_opendesk_cli channels list
86
+ deepflow_opendesk_cli routing session list
87
+ deepflow_opendesk_cli queue policies
88
+ ```
89
+
90
+ Equivalent module invocation:
91
+
92
+ ```bash
93
+ python -m deepflow_opendesk_cli docs list
94
+ ```
95
+
96
+ ## AI Agent Flow
97
+
98
+ Recommended execution loop:
99
+
100
+ ```text
101
+ 1. schema fetch or docs list
102
+ 2. docs show <endpoint-key>
103
+ 3. request/call --dry-run
104
+ 4. present plan to a human
105
+ 5. batch run / workflow run --yes
106
+ 6. verify with config list/get commands
107
+ ```
108
+
109
+ ## Batch
110
+
111
+ `batch run` accepts JSONL. Each line is a config operation:
112
+
113
+ ```jsonl
114
+ {"key":"auth.me"}
115
+ {"key":"fields.create","body":{"domain":"ticket","name":"Customer Tier","field_type":"select"}}
116
+ {"method":"PUT","path":"/api/v1/system-settings","body":{"default_language":"zh","default_timezone":"Asia/Shanghai"}}
117
+ ```
118
+
119
+ Run:
120
+
121
+ ```bash
122
+ deepflow_opendesk_cli batch run ops.jsonl --dry-run
123
+ deepflow_opendesk_cli batch run ops.jsonl --continue-on-error --output results.jsonl
124
+ ```
125
+
126
+ ## Development
127
+
128
+ ```bash
129
+ cd packages/deepflow-opendesk-cli
130
+ PYTHONPATH=src python -m pytest tests -q
131
+ python -m build
132
+ ```
133
+
134
+ ## Distribution
135
+
136
+ Releases are automated. Pushing a `v*` git tag (e.g. `v0.1.1`) triggers
137
+ `.github/workflows/release.yml`, which runs the tests, builds the sdist + wheel,
138
+ publishes `deepflow-opendesk-cli` to PyPI, and cuts a GitHub Release.
139
+
140
+ ```bash
141
+ pip install deepflow-opendesk-cli
142
+ ```
143
+
144
+ To cut a patch release, run the helper script (bumps the version across
145
+ `pyproject.toml`, `setup.py`, and `__init__.py`, then tags and pushes):
146
+
147
+ ```bash
148
+ bash .claude/skills/release-publish/scripts/release-tag.sh --dry-run
149
+ bash .claude/skills/release-publish/scripts/release-tag.sh
150
+ ```
151
+
152
+ To build locally without publishing: `python -m build`.
@@ -0,0 +1,13 @@
1
+ deepflow_opendesk_cli/__init__.py,sha256=QQUA2RQSL5navxL2IBHLJuCcYDNGu6DlVSvyG0Bj-cA,67
2
+ deepflow_opendesk_cli/__main__.py,sha256=8T1O4_06PjtF3IgGo08A184Ih4Nv0Sokp4SNQAAebBA,146
3
+ deepflow_opendesk_cli/batch.py,sha256=6b2NggZO6PZkSU0zNRqiYfEPrtRKx0QI_ALnLJx2zr4,1837
4
+ deepflow_opendesk_cli/cli.py,sha256=m_5ONJKhHBSOgUeBVJnDLL4kaP_09vlPxUeO9kpz1Ig,27751
5
+ deepflow_opendesk_cli/client.py,sha256=xfaRE8E0lhSFf8oST4xHRbwd6ysXnYntEpwiTpFUlcQ,10793
6
+ deepflow_opendesk_cli/profiles.py,sha256=1qxnwZPtOMKZuHYR2KyQrNimZiKCoZJmmoHkZvzd6n8,2683
7
+ deepflow_opendesk_cli/schema.py,sha256=LCVFLelF1HYRIwcBSjFlBWnGskRZBLb-cNZWlq744L8,13336
8
+ deepflow_opendesk_cli/workflows.py,sha256=1SOJM_m4_S0MgyiJwGZ3OsabG6yB03E56qF7mgijrVQ,1561
9
+ deepflow_opendesk_cli-0.1.0.dist-info/METADATA,sha256=7vlFUyMEu_SippEoKeVBqwYcXle_Yc5wBxnLfFSgJVo,4332
10
+ deepflow_opendesk_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
+ deepflow_opendesk_cli-0.1.0.dist-info/entry_points.txt,sha256=5mTQ__pNqudneSyStqhp1LTYH0_zWuZEwGmV4iO-Qxk,73
12
+ deepflow_opendesk_cli-0.1.0.dist-info/top_level.txt,sha256=JrIzmrPOJORIaCHKzwnFXoV1N1TmHFY3jOv-7LAzx7E,22
13
+ deepflow_opendesk_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ deepflow_opendesk_cli = deepflow_opendesk_cli.cli:main
@@ -0,0 +1 @@
1
+ deepflow_opendesk_cli