egnyte-client 0.1.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 (66) hide show
  1. egnyte_client-0.1.0/CHANGELOG.md +24 -0
  2. egnyte_client-0.1.0/LICENSE +21 -0
  3. egnyte_client-0.1.0/PKG-INFO +576 -0
  4. egnyte_client-0.1.0/README.md +545 -0
  5. egnyte_client-0.1.0/pyproject.toml +57 -0
  6. egnyte_client-0.1.0/src/egnyte_client/__init__.py +160 -0
  7. egnyte_client-0.1.0/src/egnyte_client/async_client.py +2901 -0
  8. egnyte_client-0.1.0/src/egnyte_client/cli/__init__.py +132 -0
  9. egnyte_client-0.1.0/src/egnyte_client/cli/__main__.py +4 -0
  10. egnyte_client-0.1.0/src/egnyte_client/cli/commands/__init__.py +0 -0
  11. egnyte_client-0.1.0/src/egnyte_client/cli/commands/audit.py +1226 -0
  12. egnyte_client-0.1.0/src/egnyte_client/cli/commands/auth.py +32 -0
  13. egnyte_client-0.1.0/src/egnyte_client/cli/commands/bookmarks.py +63 -0
  14. egnyte_client-0.1.0/src/egnyte_client/cli/commands/comments.py +101 -0
  15. egnyte_client-0.1.0/src/egnyte_client/cli/commands/events.py +91 -0
  16. egnyte_client-0.1.0/src/egnyte_client/cli/commands/fs.py +690 -0
  17. egnyte_client-0.1.0/src/egnyte_client/cli/commands/groups.py +178 -0
  18. egnyte_client-0.1.0/src/egnyte_client/cli/commands/links.py +138 -0
  19. egnyte_client-0.1.0/src/egnyte_client/cli/commands/metadata.py +737 -0
  20. egnyte_client-0.1.0/src/egnyte_client/cli/commands/perms.py +245 -0
  21. egnyte_client-0.1.0/src/egnyte_client/cli/commands/search.py +109 -0
  22. egnyte_client-0.1.0/src/egnyte_client/cli/commands/trash.py +183 -0
  23. egnyte_client-0.1.0/src/egnyte_client/cli/commands/users.py +138 -0
  24. egnyte_client-0.1.0/src/egnyte_client/cli/commands/webhooks.py +218 -0
  25. egnyte_client-0.1.0/src/egnyte_client/cli/context.py +47 -0
  26. egnyte_client-0.1.0/src/egnyte_client/cli/group.py +72 -0
  27. egnyte_client-0.1.0/src/egnyte_client/cli/options.py +139 -0
  28. egnyte_client-0.1.0/src/egnyte_client/cli/output.py +64 -0
  29. egnyte_client-0.1.0/src/egnyte_client/client.py +2870 -0
  30. egnyte_client-0.1.0/src/egnyte_client/errors.py +320 -0
  31. egnyte_client-0.1.0/src/egnyte_client/oauth.py +45 -0
  32. egnyte_client-0.1.0/src/egnyte_client/operations/__init__.py +11 -0
  33. egnyte_client-0.1.0/src/egnyte_client/operations/audit.py +807 -0
  34. egnyte_client-0.1.0/src/egnyte_client/operations/auth.py +54 -0
  35. egnyte_client-0.1.0/src/egnyte_client/operations/bookmark.py +100 -0
  36. egnyte_client-0.1.0/src/egnyte_client/operations/comment.py +110 -0
  37. egnyte_client-0.1.0/src/egnyte_client/operations/event.py +63 -0
  38. egnyte_client-0.1.0/src/egnyte_client/operations/filesystem.py +814 -0
  39. egnyte_client-0.1.0/src/egnyte_client/operations/generic.py +228 -0
  40. egnyte_client-0.1.0/src/egnyte_client/operations/group.py +184 -0
  41. egnyte_client-0.1.0/src/egnyte_client/operations/insights.py +23 -0
  42. egnyte_client-0.1.0/src/egnyte_client/operations/link.py +161 -0
  43. egnyte_client-0.1.0/src/egnyte_client/operations/metadata.py +473 -0
  44. egnyte_client-0.1.0/src/egnyte_client/operations/permission.py +118 -0
  45. egnyte_client-0.1.0/src/egnyte_client/operations/search.py +79 -0
  46. egnyte_client-0.1.0/src/egnyte_client/operations/trash.py +135 -0
  47. egnyte_client-0.1.0/src/egnyte_client/operations/user.py +192 -0
  48. egnyte_client-0.1.0/src/egnyte_client/operations/webhook.py +230 -0
  49. egnyte_client-0.1.0/src/egnyte_client/py.typed +0 -0
  50. egnyte_client-0.1.0/src/egnyte_client/schemas/audit.py +775 -0
  51. egnyte_client-0.1.0/src/egnyte_client/schemas/auth.py +89 -0
  52. egnyte_client-0.1.0/src/egnyte_client/schemas/bookmark.py +60 -0
  53. egnyte_client-0.1.0/src/egnyte_client/schemas/comment.py +76 -0
  54. egnyte_client-0.1.0/src/egnyte_client/schemas/event.py +253 -0
  55. egnyte_client-0.1.0/src/egnyte_client/schemas/filesystem.py +491 -0
  56. egnyte_client-0.1.0/src/egnyte_client/schemas/generic.py +73 -0
  57. egnyte_client-0.1.0/src/egnyte_client/schemas/group.py +97 -0
  58. egnyte_client-0.1.0/src/egnyte_client/schemas/insights.py +71 -0
  59. egnyte_client-0.1.0/src/egnyte_client/schemas/link.py +159 -0
  60. egnyte_client-0.1.0/src/egnyte_client/schemas/metadata.py +362 -0
  61. egnyte_client-0.1.0/src/egnyte_client/schemas/permission.py +79 -0
  62. egnyte_client-0.1.0/src/egnyte_client/schemas/search.py +259 -0
  63. egnyte_client-0.1.0/src/egnyte_client/schemas/trash.py +241 -0
  64. egnyte_client-0.1.0/src/egnyte_client/schemas/user.py +213 -0
  65. egnyte_client-0.1.0/src/egnyte_client/schemas/webhook.py +253 -0
  66. egnyte_client-0.1.0/src/egnyte_client/transport.py +341 -0
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-11-05
4
+
5
+ - `EgnyteClient` and `AsyncEgnyteClient`, method-for-method identical, over `httpx`.
6
+ - File system: `get_item`, `get_folder`, `get_folder_by_id`, `get_file`, `get_file_by_id`, `create_folder`, `upload_file`, `download_file`, `download_file_by_id`, `copy_item`, `move_item`, and `delete_item`.
7
+ - File system by ID: `copy_file_by_id`, `copy_folder_by_id`, `move_file_by_id`, `move_folder_by_id`, `delete_file_by_id`, `delete_folder_by_id`, and `get_folder_stats`.
8
+ - Locks: `lock_file`, `unlock_file`, `lock_file_by_id`, and `unlock_file_by_id`.
9
+ - Chunked upload: `upload_file_chunked` and `upload_file_chunked_by_id`, streaming bytes or a file object with Egnyte's composite checksum.
10
+ - Folder options, insights, and trash: `update_folder_options`, `list_recent_files`, `list_trash`, `count_trash`, `restore_trash_items`, and `purge_trash_items`.
11
+ - Permissions: `get_effective_permission`, `get_folder_permissions`, and `set_folder_permissions`.
12
+ - Comments and bookmarks: `create_comment`, `list_comments`, `get_comment`, `delete_comment`, `create_bookmark`, `list_bookmarks`, `get_bookmark`, and `delete_bookmark`.
13
+ - Metadata: namespaces (`list_`, `create_`, `get_`, `update_`, `delete_metadata_namespace`), keys (`add_`, `update_`, `delete_metadata_key`, `delete_metadata_key_values`, `delete_metadata_values`), and values on files and folders (`get_`/`set_file_metadata`, `get_`/`set_folder_metadata`).
14
+ - Events and webhooks: `get_event_cursor`, `list_events`, `create_webhook`, `list_webhooks`, `get_webhook`, `get_webhook_status`, `set_webhook_status`, `update_webhook`, `delete_webhook`, and `verify_webhook_authorization` for deliveries.
15
+ - Audit reporting: a `create_*_audit_report` method for each of the 14 report types, `get_audit_job`, `get_audit_report`, `download_audit_report`, `delete_audit_report`, and `stream_audit_events`.
16
+ - Links: `create_link`, `get_link`, `list_links`, and `delete_link`.
17
+ - Search: `search` over names, content, comments, and custom metadata, with folder, type, and date filters, similarity search, and `MetadataFilter`. The CLI command is `egnyte-client search`.
18
+ - Users: `list_users`, `get_user`, `create_user`, `update_user`, and `delete_user`.
19
+ - Groups: `list_groups`, `get_group`, `create_group`, `update_group`, `replace_group`, and `delete_group`.
20
+ - `get_user_info`, `get_token_info`, `revoke_token`, `refresh_access_token`, `exchange_authorization_code`, `login_with_password`, and `build_authorization_url`. A 401 refreshes the token once when refresh credentials are configured, and `on_token_refresh` receives the new pair.
21
+ - Calls refused by the per-second rate limit (HTTP 429, spike arrest) are retried after `Retry-After`. The daily quota raises `EgnyteQuotaExceededError` at once, and any other 429 is retried only when it asks for a wait of 5 seconds or less.
22
+ - Logging follows the library pattern: the `egnyte_client` logger has only a `NullHandler` and no level set. Requests log at DEBUG, retries and token refreshes at INFO; the CLI's `--log` prints them to stderr.
23
+ - Errors: every exception is an `EgnyteError`. `EgnyteAPIError` reads each of Egnyte's error body shapes into a message and code and keeps the response; `EgnyteValidationError` (400), `EgnyteAuthError` (401), `EgnyteForbiddenError` (403), `EgnyteNotFoundError` (404), `EgnyteConflictError` (409), `EgnyteRateLimitError` (429, with `retry_after`) and its `EgnyteQuotaExceededError` (the daily quota), and `EgnyteServerError` (5xx) subclass it, and `EgnyteConfigurationError` reports a missing domain, token, or credential.
24
+ - The `egnyte-client` CLI, with a command for every client method except the token grants: `auth`, `fs`, `search`, `trash`, `links`, `perms`, `comments`, `bookmarks`, `metadata`, `users`, `groups`, `events`, `webhooks`, and `audit`. Destructive commands confirm first (`--yes` skips), results print as JSON, and `EGNYTE_CLI_DISABLE` hides commands.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SB&O Inc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,576 @@
1
+ Metadata-Version: 2.4
2
+ Name: egnyte-client
3
+ Version: 0.1.0
4
+ Summary: Egnyte Public API Python Client
5
+ Keywords: egnyte,api,client,cli,file-sharing,storage
6
+ Author: SB&O Inc
7
+ Author-email: SB&O Inc <contact@sboinc.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Programming Language :: Python :: 3.15
19
+ Classifier: Topic :: Communications :: File Sharing
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Dist: click>=8.0.0
23
+ Requires-Dist: httpx>=0.20.0
24
+ Requires-Dist: pydantic>=2.11.0
25
+ Requires-Python: >=3.12
26
+ Project-URL: Download, https://github.com/sbo-inc/egnyte-client/releases
27
+ Project-URL: Homepage, https://github.com/sbo-inc/egnyte-client
28
+ Project-URL: Issues, https://github.com/sbo-inc/egnyte-client/issues
29
+ Project-URL: Repository, https://github.com/sbo-inc/egnyte-client
30
+ Description-Content-Type: text/markdown
31
+
32
+ [![CI](https://github.com/sbo-inc/egnyte-client/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/sbo-inc/egnyte-client/actions/workflows/ci.yaml)
33
+ [![PyPI](https://img.shields.io/pypi/v/egnyte-client.svg)](https://pypi.org/project/egnyte-client/)
34
+ [![Python versions](https://img.shields.io/pypi/pyversions/egnyte-client.svg)](https://pypi.org/project/egnyte-client/)
35
+ [![License](https://img.shields.io/pypi/l/egnyte-client.svg)](https://github.com/sbo-inc/egnyte-client/blob/main/LICENSE)
36
+
37
+ # Egnyte Python client
38
+
39
+ An unofficial typed Python client and command-line interface for the [Egnyte Public API](https://developers.egnyte.com/integration/cfs/api-docs/getting-started). No affiliation with Egnyte is implied or intended.
40
+
41
+ The package wraps Egnyte's REST APIs in a fully type-hinted client built on [Pydantic](https://docs.pydantic.dev/), plus an `egnyte-client` CLI for quick access from the terminal. Responses are validated into predictable Python objects, so you can work with files, folders, search, links, users, and groups without hand-rolling requests.
42
+
43
+ ## Features
44
+
45
+ - **Typed models.** Every response is parsed into Pydantic models with descriptive fields, and Egnyte's timestamps become `datetime`s.
46
+ - **Python client and CLI.** Use it as a library or from the shell via `egnyte-client`.
47
+ - **Sync and async.** `EgnyteClient` and `AsyncEgnyteClient` expose the same methods over `httpx`.
48
+ - **Sensible defaults.** Paths are encoded for you, calls refused by the rate limit are retried after `Retry-After`, and an expired token is refreshed when refresh credentials are configured.
49
+ - **The core platform.** Files and folders (including chunked upload, locks, and trash), search, links, permissions, comments, bookmarks, metadata, events, webhooks, users, groups, and audit reporting.
50
+ - **Typed errors.** Egnyte's error bodies come in several shapes. Each is raised as an `EgnyteError` with a status, message, and code. `EgnyteNotFoundError`, `EgnyteAuthError`, and `EgnyteRateLimitError` are its subclasses.
51
+
52
+ ## Installation
53
+
54
+ The package is published on PyPI as [`egnyte-client`](https://pypi.org/project/egnyte-client/) and imported as `egnyte_client`:
55
+
56
+ ```bash
57
+ pip install egnyte-client
58
+ # or, with uv:
59
+ uv add egnyte-client
60
+ ```
61
+
62
+ Requires Python 3.12+.
63
+
64
+ ## Configuration
65
+
66
+ Settings are read from environment variables, or can be passed directly to `EgnyteClient`:
67
+
68
+ | Variable | Description |
69
+ | --- | --- |
70
+ | `EGNYTE_DOMAIN` | Your domain: `acme`, `acme.egnyte.com`, or `https://acme.egnyte.com`. |
71
+ | `EGNYTE_ACCESS_TOKEN` | OAuth 2.0 bearer token. |
72
+ | `EGNYTE_REFRESH_TOKEN` | Optional. Refreshes an expired access token, together with the next two. |
73
+ | `EGNYTE_CLIENT_ID` | Optional. Your API key. |
74
+ | `EGNYTE_CLIENT_SECRET` | Optional. Your API secret. |
75
+
76
+ ```bash
77
+ export EGNYTE_DOMAIN="acme"
78
+ export EGNYTE_ACCESS_TOKEN="your-access-token"
79
+ ```
80
+
81
+ ### Getting a token
82
+
83
+ Register an application at [developers.egnyte.com](https://developers.egnyte.com/) to get an API key and secret. Then complete one of the OAuth 2.0 flows in [Egnyte's authentication docs](https://developers.egnyte.com/integration/cfs/api-docs/authentication) against `https://<domain>.egnyte.com/puboauth/token`. Use the authorization code flow for public apps, or the resource-owner password flow for internal apps. Access tokens last 30 days.
84
+
85
+ The client can run the flow for you:
86
+
87
+ ```python
88
+ from egnyte_client import EgnyteClient, build_authorization_url
89
+
90
+ url = build_authorization_url(
91
+ "acme",
92
+ client_id,
93
+ "https://app.example.com/callback",
94
+ scope=["Egnyte.filesystem"],
95
+ state=state,
96
+ )
97
+ # ... send the user to `url`, then take `code` from the redirect:
98
+ client = EgnyteClient("acme", client_id=client_id, client_secret=client_secret)
99
+ token = client.exchange_authorization_code(code, "https://app.example.com/callback")
100
+
101
+ # Internal API keys can use the password grant instead:
102
+ token = client.login_with_password(username, password)
103
+ ```
104
+
105
+ `get_token_info()` reports a token's user, domain, and scopes, and `revoke_token()` revokes it.
106
+
107
+ When `refresh_token`, `client_id`, and `client_secret` are all set, a call that fails with 401 refreshes the token once and is retried. Egnyte rotates the refresh token on every refresh, so the old refresh token stops working: pass `on_token_refresh` to save each new pair where the next run will find it. For example, with the pair kept in a JSON file:
108
+
109
+ ```python
110
+ from pathlib import Path
111
+
112
+ from egnyte_client import EgnyteClient, OAuthToken
113
+
114
+ TOKEN_FILE = Path("egnyte-token.json") # keep it out of version control
115
+
116
+
117
+ def save_token(token: OAuthToken) -> None:
118
+ TOKEN_FILE.write_text(token.model_dump_json())
119
+
120
+
121
+ # The first pair comes from exchange_authorization_code() or
122
+ # login_with_password() above; save it with save_token(token).
123
+ token = OAuthToken.model_validate_json(TOKEN_FILE.read_text())
124
+
125
+ client = EgnyteClient(
126
+ "acme",
127
+ access_token=token.access_token,
128
+ refresh_token=token.refresh_token,
129
+ client_id="...",
130
+ client_secret="...",
131
+ on_token_refresh=save_token, # called with each new OAuthToken
132
+ )
133
+ ```
134
+
135
+ ### Rate limits, timeouts and retries
136
+
137
+ Egnyte allows 2 calls per second per token, and between 1,000 and 4,000 calls per day depending on the plan. A call refused by the per-second limit (HTTP 429, a spike-arrest violation) is retried after its `Retry-After` (1 second if absent), up to `rate_limit_retries` times (default 5). The gateway refuses such a call before it reaches Egnyte, so retrying a write is safe. The daily quota is never retried: it raises `EgnyteQuotaExceededError` at once, since waiting seconds will not help and the quota resets at 00:00 UTC. Any other 429 is retried only when it asks for a wait of 5 seconds or less, so a long `Retry-After` never blocks the caller.
138
+
139
+ Every request carries a timeout (default `(5, 30)` seconds for connect and read). Pass `timeout=` to override it (a single float, a `(connect, read)` tuple, or `None` to disable). Pass `retries=` to retry connection-establishment failures:
140
+
141
+ ```python
142
+ client = EgnyteClient(timeout=60, retries=3, rate_limit_retries=10)
143
+ ```
144
+
145
+ The CLI reads `EGNYTE_API_TIMEOUT` (seconds), `EGNYTE_API_RETRIES` (count), and `EGNYTE_API_HEADERS` (a JSON object of extra headers) for the same behaviour.
146
+
147
+ ## Quick start
148
+
149
+ ### Python
150
+
151
+ ```python
152
+ from egnyte_client import EgnyteClient, EgnyteNotFoundError
153
+
154
+ with EgnyteClient() as client:
155
+ me = client.get_user_info()
156
+
157
+ # Folders and files
158
+ client.create_folder("/Shared/Reports/2026")
159
+ uploaded = client.upload_file("/Shared/Reports/2026/summary.pdf", pdf_bytes)
160
+ folder = client.get_folder("/Shared/Reports/2026")
161
+ for file in folder.files:
162
+ print(file.name, file.size, file.last_modified)
163
+ data = client.download_file("/Shared/Reports/2026/summary.pdf")
164
+ client.move_item(
165
+ "/Shared/Reports/2026/summary.pdf", "/Shared/Reports/2026/final.pdf"
166
+ )
167
+
168
+ # Search names, content, comments, and metadata
169
+ page = client.search("quarterly revenue", folder="/Shared/Reports", type="FILE")
170
+ for hit in page.results:
171
+ print(hit.path, hit.score, hit.snippet)
172
+
173
+ # Links
174
+ link = client.create_link(
175
+ "/Shared/Reports/2026/final.pdf", "file", accessibility="domain"
176
+ )
177
+ print(link.url)
178
+ client.delete_link(link.id)
179
+
180
+ # Groups
181
+ group = client.create_group("Reporting", [me.id])
182
+ client.update_group(group.id, add_member_ids=[42])
183
+ client.delete_group(group.id)
184
+
185
+ try:
186
+ client.get_file("/Shared/missing.txt")
187
+ except EgnyteNotFoundError:
188
+ ...
189
+ ```
190
+
191
+ ### Python (async)
192
+
193
+ `AsyncEgnyteClient` mirrors `EgnyteClient` method for method, with the same arguments and return types, awaited. Share one instance across tasks so they reuse its connection pool and tokens. Keep fan-out modest, because Egnyte allows two calls per second:
194
+
195
+ ```python
196
+ import asyncio
197
+
198
+ from egnyte_client import AsyncEgnyteClient
199
+
200
+
201
+ async def main() -> None:
202
+ async with AsyncEgnyteClient() as client:
203
+ folder = await client.get_folder("/Shared/Reports")
204
+ contents = await asyncio.gather(
205
+ *(client.download_file(file.path) for file in folder.files[:4])
206
+ )
207
+
208
+
209
+ asyncio.run(main())
210
+ ```
211
+
212
+ ### CLI
213
+
214
+ ```console
215
+ $ egnyte-client auth whoami
216
+ $ egnyte-client fs ls /Shared/Reports --count 20
217
+ $ egnyte-client fs upload ./summary.pdf /Shared/Reports/
218
+ $ egnyte-client fs download /Shared/Reports/summary.pdf -o -
219
+ $ egnyte-client search "quarterly revenue" --folder /Shared/Reports --type file --in content
220
+ $ egnyte-client links create /Shared/Reports/summary.pdf --type file --accessibility domain
221
+ $ egnyte-client groups create Reporting --member 8
222
+ $ egnyte-client trash list --count 20
223
+ $ egnyte-client perms set /Shared/Reports --group Finance=Viewer
224
+ $ egnyte-client metadata set project --folder-id FOLDER_ID --value status=active
225
+ $ egnyte-client audit create files 2026-11-01 2026-11-05 --folder /Shared
226
+ $ egnyte-client --log users list --email jdoe@acme.com
227
+ ```
228
+
229
+ Every client method has a command except the token grants (the CLI reads its token from the environment); the API reference below lists each one. `egnyte-client <group> --help` lists every option.
230
+
231
+ - **Output.** Results print as indented JSON, so they pipe into `jq`; downloads write raw bytes (`-o -` for stdout); a bare value, such as a lock token or a permission level, prints alone so a script can capture it.
232
+ - **Confirmation.** Commands that delete, purge, replace, or change permissions ask first, naming what will change; `-y`/`--yes` skips the prompt. Prompts go to stderr.
233
+ - **Structured input.** Options that take a structure, such as a namespace's keys or metadata values, take JSON (`--keys '[...]'`), checked before anything is sent.
234
+ - **Secrets.** The webhook `--auth-header` can come from `EGNYTE_WEBHOOK_AUTH_HEADER` instead, keeping it out of shell history, and is never printed back.
235
+ - **Disabling commands.** Set `EGNYTE_CLI_DISABLE` to a comma-separated list of dotted command paths (for example `users.delete,groups,metadata.namespaces.delete`) to hide and refuse those commands.
236
+
237
+ ## Usage
238
+
239
+ ### Pagination
240
+
241
+ List methods return one page, and the page size is capped by Egnyte. Each API family pages in its own style:
242
+
243
+ | API | Arguments | Cap |
244
+ | --- | --- | --- |
245
+ | Links, comments, bookmarks, audit report rows | `offset`, `count` | varies |
246
+ | Users, groups | `start_index` (1-based), `count` | 100 |
247
+ | Folder listing | `offset`, `count`; the folder's `total_count` says how many children there are | |
248
+ | Search | `offset`, `count`; continue while `has_more` | 20 |
249
+ | Trash | `offset`, `count`; continue while `has_more` | |
250
+ | Events | start from `get_event_cursor().latest_event_id`; each page's `latest_id` is the next start | |
251
+ | Audit stream | `start_date`, then each page's `next_cursor` while `more_events` | |
252
+
253
+ Every page is an API call, and Egnyte's daily quota is between 1,000 and 4,000 calls, so page deliberately.
254
+
255
+ ### Errors
256
+
257
+ Every error this package raises is an `EgnyteError`:
258
+
259
+ | Exception | When |
260
+ | --- | --- |
261
+ | `EgnyteConfigurationError` | The domain, token, or OAuth credentials a call needs are missing. Also a `ValueError`. |
262
+ | `EgnyteAPIError` | Egnyte answered with an error status. Carries `status_code`, `message`, `code`, `body`, and `response`. |
263
+ | `EgnyteValidationError` | 400: the request's content was refused. |
264
+ | `EgnyteAuthError` | 401: the token is missing, invalid, expired, or revoked. |
265
+ | `EgnyteForbiddenError` | 403: not permitted, a missing scope, or (for folders) already exists. |
266
+ | `EgnyteNotFoundError` | 404: no such path, ID, link, user, or group. |
267
+ | `EgnyteConflictError` | 409: conflicts with existing state, such as a duplicate username. |
268
+ | `EgnyteRateLimitError` | 429: the per-second limit after every retry, or a refusal not worth retrying; `retry_after` is Egnyte's requested wait. |
269
+ | `EgnyteQuotaExceededError` | 429: the token's daily quota is used up (raised at once; resets at 00:00 UTC). A subclass of `EgnyteRateLimitError`. |
270
+ | `EgnyteServerError` | 5xx. |
271
+
272
+ Egnyte words its errors in several body shapes depending on the API; each is read into the same `message` and `code`:
273
+
274
+ ```python
275
+ from egnyte_client import EgnyteForbiddenError, EgnyteNotFoundError
276
+
277
+ try:
278
+ client.create_folder("/Shared/Reports")
279
+ except EgnyteForbiddenError:
280
+ pass # already exists
281
+ try:
282
+ client.get_file("/Shared/missing.txt")
283
+ except EgnyteNotFoundError as error:
284
+ print(error.message)
285
+ ```
286
+
287
+ Arguments the client can check before sending (for example, two mutually exclusive filters) raise a plain `ValueError` without a request.
288
+
289
+ ### Logging
290
+
291
+ The package logs to the `egnyte_client` logger and configures nothing itself: it attaches only a `NullHandler`, so records appear only where your application's logging sends them. Each request is logged at DEBUG (`GET /pubapi/v1/userinfo -> 200`); rate-limit retries and token refreshes at INFO.
292
+
293
+ ```python
294
+ import logging
295
+
296
+ logging.basicConfig(level=logging.INFO)
297
+ logging.getLogger("egnyte_client").setLevel(logging.DEBUG) # include each request
298
+ ```
299
+
300
+ The CLI's `--log` flag prints the same records to stderr.
301
+
302
+ ### Uploads and downloads
303
+
304
+ `upload_file()` sends content in one request and suits files up to about 100 MB. For larger files, `upload_file_chunked()` takes bytes or an open binary file and streams it in chunks; the CLI's `fs upload` switches to it on its own. `download_file()` returns the content as bytes.
305
+
306
+ ### Background jobs
307
+
308
+ Audit reports are generated asynchronously:
309
+
310
+ ```python
311
+ job = client.create_file_audit_report(start, end, folders=["/Shared"])
312
+ status = client.get_audit_job(job.id) # poll at most every 2 minutes
313
+ if status.is_complete:
314
+ report = client.get_audit_report(status.report_type, status.report_id)
315
+ client.delete_audit_report(status.report_type, status.report_id)
316
+ ```
317
+
318
+ ## Design
319
+
320
+ ### Operations, transport, and two clients
321
+
322
+ Each API call is described once, in `egnyte_client.operations`, as an `Operation`: method, path, query, body, and the parser that turns the response into a model. Nothing there performs I/O. `EgnyteClient` and `AsyncEgnyteClient` only put operations on the wire, through a shared transport that holds configuration, token refresh, the rate-limit retry, and error mapping. The two clients therefore have the same methods, arguments, docstrings, and return types, which a test checks.
323
+
324
+ ### Live behaviour over the documentation
325
+
326
+ Where Egnyte's live API differs from its documentation, the models and methods follow the live API. For example, date values come back as epoch milliseconds, the by-ID chunked upload endpoint is unusable, and some status codes differ. Response models ignore fields they do not declare, so a new field from Egnyte never breaks parsing. Audit report rows and events also keep undeclared fields in `model_extra`, since their columns vary by report type.
327
+
328
+ ## Reference Documentation
329
+
330
+ This client follows (to the extent possible) the API documentation Egnyte provides at [developers.egnyte.com](https://developers.egnyte.com/integration/cfs/api-docs/getting-started).
331
+
332
+ ## API reference
333
+
334
+ Each section maps client methods to the Egnyte endpoints they call, and to the CLI command where there is one. `AsyncEgnyteClient` exposes every method as a coroutine of the same name.
335
+
336
+ ### Auth
337
+
338
+ Docs: [Authentication](https://developers.egnyte.com/integration/cfs/api-docs/authentication)
339
+
340
+ | Client | Endpoint | CLI |
341
+ | --- | --- | --- |
342
+ | `build_authorization_url()` (function) | `GET /puboauth/token` | library only |
343
+ | `exchange_authorization_code()` | `POST /puboauth/token` | library only |
344
+ | `login_with_password()` | `POST /puboauth/token` | library only |
345
+ | `refresh_access_token()` | `POST /puboauth/token` | library only |
346
+ | `get_user_info()` | `GET /pubapi/v1/userinfo` | `egnyte-client auth whoami` |
347
+ | `get_token_info()` | `GET /pubapi/v1/whoami` | `egnyte-client auth token-info` |
348
+ | `revoke_token()` | `POST /pubapi/v1/tokens/revoke` | library only |
349
+
350
+ `build_authorization_url()` is a module function, as it makes no request. The token grants are library-only: the CLI reads its token from `EGNYTE_ACCESS_TOKEN`. Each grant adopts the new token pair and passes it to `on_token_refresh`.
351
+
352
+ ### File system
353
+
354
+ Docs: [File System Management](https://developers.egnyte.com/integration/cfs/api-docs/file-system-management) · [Folder Options](https://developers.egnyte.com/integration/cfs/api-docs/folder-options-api)
355
+
356
+ | Client | Endpoint | CLI |
357
+ | --- | --- | --- |
358
+ | `get_folder()` | `GET /pubapi/v1/fs/{path}` | `egnyte-client fs ls` |
359
+ | `get_folder_by_id()` | `GET /pubapi/v1/fs/ids/folder/{id}` | `egnyte-client fs ls --folder-id ID` |
360
+ | `get_item()` | `GET /pubapi/v1/fs/{path}` | `egnyte-client fs stat` |
361
+ | `get_file()` | `GET /pubapi/v1/fs/{path}` | `egnyte-client fs stat --versions` |
362
+ | `get_file_by_id()` | `GET /pubapi/v1/fs/ids/file/{id}` | `egnyte-client fs stat --file-id ID` |
363
+ | `get_folder_stats()` | `GET /pubapi/v1/fs/ids/folder/{id}/stats` | `egnyte-client fs stats PATH` |
364
+ | `create_folder()` | `POST /pubapi/v1/fs/{path}` | `egnyte-client fs mkdir` |
365
+ | `update_folder_options()` | `PATCH /pubapi/v1/fs/{path}` | `egnyte-client fs options PATH` |
366
+ | `upload_file()` | `POST /pubapi/v1/fs-content/{path}` | `egnyte-client fs upload` |
367
+ | `upload_file_chunked()` | `POST /pubapi/v1/fs-content-chunked/{path}` | `egnyte-client fs upload LOCAL DEST (large files)` |
368
+ | `upload_file_chunked_by_id()` | `GET /pubapi/v1/fs/ids/file/{id}`, then `POST /pubapi/v1/fs-content-chunked/{path}` | `egnyte-client fs upload LOCAL --file-id ID (large files)` |
369
+ | `download_file()` | `GET /pubapi/v1/fs-content/{path}` | `egnyte-client fs download` |
370
+ | `download_file_by_id()` | `GET /pubapi/v1/fs-content/ids/file/{id}` | `egnyte-client fs download --file-id ID` |
371
+ | `copy_item()` | `POST /pubapi/v1/fs/{path}` | `egnyte-client fs cp` |
372
+ | `move_item()` | `POST /pubapi/v1/fs/{path}` | `egnyte-client fs mv` |
373
+ | `copy_file_by_id()` | `POST /pubapi/v1/fs/ids/file/{id}` | `egnyte-client fs cp --file-id ID DEST_FOLDER_ID` |
374
+ | `move_file_by_id()` | `POST /pubapi/v1/fs/ids/file/{id}` | `egnyte-client fs mv --file-id ID DEST_FOLDER_ID` |
375
+ | `copy_folder_by_id()` | `POST /pubapi/v1/fs/ids/folder/{id}` | `egnyte-client fs cp --folder-id ID DEST_FOLDER_ID` |
376
+ | `move_folder_by_id()` | `POST /pubapi/v1/fs/ids/folder/{id}` | `egnyte-client fs mv --folder-id ID DEST_FOLDER_ID` |
377
+ | `lock_file()` | `POST /pubapi/v1/fs/{path}` | `egnyte-client fs lock PATH` |
378
+ | `unlock_file()` | `POST /pubapi/v1/fs/{path}` | `egnyte-client fs unlock PATH --lock-token T` |
379
+ | `lock_file_by_id()` | `POST /pubapi/v1/fs/ids/file/{id}` | `egnyte-client fs lock --file-id ID` |
380
+ | `unlock_file_by_id()` | `POST /pubapi/v1/fs/ids/file/{id}` | `egnyte-client fs unlock --file-id ID --lock-token T` |
381
+ | `delete_item()` | `DELETE /pubapi/v1/fs/{path}` | `egnyte-client fs rm` |
382
+ | `delete_file_by_id()` | `DELETE /pubapi/v1/fs/ids/file/{id}` | `egnyte-client fs rm --file-id ID` |
383
+ | `delete_folder_by_id()` | `DELETE /pubapi/v1/fs/ids/folder/{id}` | `egnyte-client fs rm --folder-id ID` |
384
+
385
+ `upload_file()` sends one request and suits files up to about 100 MB. `upload_file_chunked()` streams bytes or a binary file object in chunks of 10 MiB to 1 GiB (default 100 MiB), with the composite checksum Egnyte verifies. Egnyte refuses content under 10 MiB on the chunked endpoint, so use `upload_file()` for small files. Egnyte's documented by-ID chunked endpoint rejects every upload, so `upload_file_chunked_by_id()` looks up the file's path (one extra call) and uploads to it. For chunked uploads, Egnyte stores the composite checksum, not a plain SHA-512. Downloads are read into memory.
386
+
387
+ ### Search
388
+
389
+ Docs: [Search](https://developers.egnyte.com/integration/cfs/api-docs/search-api)
390
+
391
+ | Client | Endpoint | CLI |
392
+ | --- | --- | --- |
393
+ | `search()` | `POST /pubapi/v2/search` | `egnyte-client search` |
394
+
395
+ `search` matches file and folder names, file content, comments, and folder descriptions. It also takes:
396
+
397
+ - Filters: `folder`, `type`, and modified and uploaded date ranges.
398
+ - `metadata_filters`, a list of `MetadataFilter`s, for custom metadata.
399
+ - `similar_to_entry_ids` and `similar_to_text`, for similarity search.
400
+
401
+ The query must be 3 to 250 characters. Results arrive 20 at a time at most, best match first. Page with `offset` while `has_more` is true:
402
+
403
+ ```python
404
+ from egnyte_client import MetadataFilter
405
+
406
+ offset = 0
407
+ while True:
408
+ page = client.search(
409
+ "invoice",
410
+ metadata_filters=[
411
+ MetadataFilter(namespace="finance", key="status", value="paid")
412
+ ],
413
+ offset=offset,
414
+ count=20,
415
+ )
416
+ handle(page.results)
417
+ if not page.has_more:
418
+ break
419
+ offset += page.count
420
+ ```
421
+
422
+ Each page is one API call, so mind the daily quota when paging deep.
423
+
424
+ ### Trash and insights
425
+
426
+ Docs: [Trash](https://developers.egnyte.com/integration/cfs/api-docs/trash-api) · [User Insights](https://developers.egnyte.com/integration/cfs/api-docs/user-insights-api)
427
+
428
+ | Client | Endpoint | CLI |
429
+ | --- | --- | --- |
430
+ | `list_trash()` | `GET /pubapi/v2/fs/trash` | `egnyte-client trash list` |
431
+ | `count_trash()` | `GET /pubapi/v2/fs/trash/totalcount` | `egnyte-client trash count` |
432
+ | `restore_trash_items()` | `POST /pubapi/v1/fs/trash` | `egnyte-client trash restore ITEM_ID...` |
433
+ | `purge_trash_items()` | `POST /pubapi/v1/fs/trash` | `egnyte-client trash purge ITEM_ID...` |
434
+ | `list_recent_files()` | `GET /pubapi/v1/insights/files` | `egnyte-client fs recent` |
435
+
436
+ Restore and purge take up to 10 item IDs and return one result per item, so a partial failure does not raise. Purging cannot be undone.
437
+
438
+ ### Permissions
439
+
440
+ Docs: [Permissions](https://developers.egnyte.com/integration/cfs/api-docs/permissions-api)
441
+
442
+ | Client | Endpoint | CLI |
443
+ | --- | --- | --- |
444
+ | `get_effective_permission()` | `GET /pubapi/v1/perms/user/{username}` | `egnyte-client perms effective PATH` |
445
+ | `get_folder_permissions()` | `GET /pubapi/v2/perms/{path}` | `egnyte-client perms get PATH` |
446
+ | `set_folder_permissions()` | `POST /pubapi/v2/perms/{path}` | `egnyte-client perms set PATH` |
447
+
448
+ In `set_folder_permissions()`, mapping a user or group to `None` removes its permission. Egnyte rejects top-level paths such as `/Shared` (the path needs at least two segments).
449
+
450
+ ### Comments and bookmarks
451
+
452
+ Docs: [Comments](https://developers.egnyte.com/integration/cfs/api-docs/comments-api) · [Bookmarks](https://developers.egnyte.com/integration/cfs/api-docs/bookmarks-api)
453
+
454
+ | Client | Endpoint | CLI |
455
+ | --- | --- | --- |
456
+ | `create_comment()` | `POST /pubapi/v1/notes` | `egnyte-client comments create PATH MESSAGE` |
457
+ | `list_comments()` | `GET /pubapi/v1/notes` | `egnyte-client comments list` |
458
+ | `get_comment()` | `GET /pubapi/v1/notes/{id}` | `egnyte-client comments get ID` |
459
+ | `delete_comment()` | `DELETE /pubapi/v1/notes/{id}` | `egnyte-client comments delete ID` |
460
+ | `create_bookmark()` | `POST /pubapi/v1/bookmarks` | `egnyte-client bookmarks create PATH` |
461
+ | `list_bookmarks()` | `GET /pubapi/v1/bookmarks` | `egnyte-client bookmarks list` |
462
+ | `get_bookmark()` | `GET /pubapi/v1/bookmarks/{id}` | `egnyte-client bookmarks get ID` |
463
+ | `delete_bookmark()` | `DELETE /pubapi/v1/bookmarks/{id}` | `egnyte-client bookmarks delete ID` |
464
+
465
+ ### Metadata
466
+
467
+ Docs: [Metadata](https://developers.egnyte.com/integration/cfs/api-docs/metadata-api)
468
+
469
+ | Client | Endpoint | CLI |
470
+ | --- | --- | --- |
471
+ | `list_metadata_namespaces()` | `GET /pubapi/v1/properties/namespace` | `egnyte-client metadata namespaces list` |
472
+ | `create_metadata_namespace()` | `POST /pubapi/v1/properties/namespace` | `egnyte-client metadata namespaces create NAME --keys JSON` |
473
+ | `get_metadata_namespace()` | `GET /pubapi/v1/properties/namespace/{ns}` | `egnyte-client metadata namespaces get NAMESPACE` |
474
+ | `update_metadata_namespace()` | `PATCH /pubapi/v1/properties/namespace/{ns}` | `egnyte-client metadata namespaces update NAMESPACE` |
475
+ | `delete_metadata_namespace()` | `DELETE /pubapi/v1/properties/namespace/{ns}` | `egnyte-client metadata namespaces delete NAMESPACE` |
476
+ | `add_metadata_key()` | `POST /pubapi/v1/properties/namespace/{ns}/keys` | `egnyte-client metadata keys add NAMESPACE KEY` |
477
+ | `update_metadata_key()` | `PATCH /pubapi/v1/properties/namespace/{ns}/keys/{key}` | `egnyte-client metadata keys update NAMESPACE KEY` |
478
+ | `delete_metadata_key()` | `DELETE /pubapi/v1/properties/namespace/{ns}/keys/{key}` | `egnyte-client metadata keys delete NAMESPACE KEY` |
479
+ | `delete_metadata_key_values()` | `PATCH /pubapi/v1/properties/namespace/{ns}/keys/{key}/data` | `egnyte-client metadata keys delete-values NAMESPACE KEY VALUE...` |
480
+ | `delete_metadata_values()` | `PATCH /pubapi/v1/properties/namespace/{ns}/keys/data` | `egnyte-client metadata namespaces delete-values NAMESPACE` |
481
+ | `get_file_metadata()` | `GET /pubapi/v1/fs/ids/file/{id}/properties/{ns}` | `egnyte-client metadata get NAMESPACE --file-id ID` |
482
+ | `set_file_metadata()` | `PUT /pubapi/v1/fs/ids/file/{id}/properties/{ns}` | `egnyte-client metadata set NAMESPACE --file-id ID` |
483
+ | `get_folder_metadata()` | `GET /pubapi/v1/fs/ids/folder/{id}/properties/{ns}` | `egnyte-client metadata get NAMESPACE --folder-id ID` |
484
+ | `set_folder_metadata()` | `PUT /pubapi/v1/fs/ids/folder/{id}/properties/{ns}` | `egnyte-client metadata set NAMESPACE --folder-id ID` |
485
+
486
+ Date values go over the wire as epoch milliseconds; pass a `date` or `datetime`. Setting a value to `None` removes it. Pass `force=True` to the deletes to remove definitions that are in use. To search by metadata, use `search(metadata_filters=...)`.
487
+
488
+ ### Events and webhooks
489
+
490
+ Docs: [Events](https://developers.egnyte.com/integration/cfs/api-docs/events-api) · [Webhooks](https://developers.egnyte.com/integration/cfs/api-docs/webhooks-api)
491
+
492
+ | Client | Endpoint | CLI |
493
+ | --- | --- | --- |
494
+ | `get_event_cursor()` | `GET /pubapi/v1/events/cursor` | `egnyte-client events cursor` |
495
+ | `list_events()` | `GET /pubapi/v2/events` | `egnyte-client events list START_ID` |
496
+ | `create_webhook()` | `POST /pubapi/v1/webhooks` | `egnyte-client webhooks create URL` |
497
+ | `list_webhooks()` | `GET /pubapi/v1/webhooks` | `egnyte-client webhooks list` |
498
+ | `get_webhook()` | `GET /pubapi/v1/webhooks/{id}/details` | `egnyte-client webhooks get ID` |
499
+ | `get_webhook_status()` | `GET /pubapi/v1/webhooks/{id}/status` | `egnyte-client webhooks status ID` |
500
+ | `set_webhook_status()` | `POST /pubapi/v1/webhooks/{id}/status` | `egnyte-client webhooks enable ID / disable ID` |
501
+ | `update_webhook()` | `PUT /pubapi/v1/webhooks/{id}` | `egnyte-client webhooks update ID URL` |
502
+ | `delete_webhook()` | `DELETE /pubapi/v1/webhooks/{id}` | `egnyte-client webhooks delete ID` |
503
+
504
+ `list_events()` returns a page whose `latest_id` is the cursor for the next call. Egnyte sends back the `auth_header` registered with a webhook as each delivery's `Authorization` header; check it with `verify_webhook_authorization()` from `egnyte_client.operations.webhook`, and parse delivery bodies with the `WebhookDelivery` model. Webhook calls need the `Egnyte.webhooks` scope.
505
+
506
+ ### Audit reporting
507
+
508
+ Docs: [Audit Reporting v1](https://developers.egnyte.com/integration/cfs/api-docs/audit-reporting-api/v1) · [Audit Reporting v2](https://developers.egnyte.com/integration/cfs/api-docs/audit-reporting-api/v2)
509
+
510
+ | Client | Endpoint | CLI |
511
+ | --- | --- | --- |
512
+ | `create_login_audit_report()` | `POST /pubapi/v1/audit/logins` | `egnyte-client audit create logins START END` |
513
+ | `create_file_audit_report()` | `POST /pubapi/v1/audit/files` | `egnyte-client audit create files START END` |
514
+ | `create_permissions_audit_report()` | `POST /pubapi/v1/audit/permissions` | `egnyte-client audit create permissions START END` |
515
+ | `create_user_audit_report()` | `POST /pubapi/v1/audit/users` | `egnyte-client audit create users START END` |
516
+ | `create_group_audit_report()` | `POST /pubapi/v1/audit/groups` | `egnyte-client audit create groups START END` |
517
+ | `create_workgroup_settings_audit_report()` | `POST /pubapi/v1/audit/workgroup-settings` | `egnyte-client audit create workgroup-settings START END` |
518
+ | `create_workflow_audit_report()` | `POST /pubapi/v1/audit/workflows` | `egnyte-client audit create workflows START END` |
519
+ | `create_workflow_template_audit_report()` | `POST /pubapi/v1/audit/workflow-templates` | `egnyte-client audit create workflow-templates START END` |
520
+ | `create_quality_docs_audit_report()` | `POST /pubapi/v1/audit/quality-docs` | `egnyte-client audit create quality-docs START END` |
521
+ | `create_quality_docs_categories_audit_report()` | `POST /pubapi/v1/audit/quality-docs-categories` | `egnyte-client audit create quality-docs-categories START END` |
522
+ | `create_quality_docs_courses_audit_report()` | `POST /pubapi/v1/audit/quality-docs-courses` | `egnyte-client audit create quality-docs-courses START END` |
523
+ | `create_etmf_audit_report()` | `POST /pubapi/v1/audit/etmf` | `egnyte-client audit create etmf START END` |
524
+ | `create_snapshot_restore_audit_report()` | `POST /pubapi/v1/audit/snapshot-restore` | `egnyte-client audit create snapshot-restore START END` |
525
+ | `create_upload_requests_audit_report()` | `POST /pubapi/v1/audit/upload-requests` | `egnyte-client audit create upload-requests START END` |
526
+ | `get_audit_job()` | `GET /pubapi/v1/audit/jobs/{id}` | `egnyte-client audit job JOB_ID` |
527
+ | `get_audit_report()` | `GET /pubapi/v1/audit/{type}/{id}` | `egnyte-client audit get TYPE REPORT_ID` |
528
+ | `download_audit_report()` | `GET /pubapi/v1/audit/{type}/{id}` | `egnyte-client audit download TYPE REPORT_ID` |
529
+ | `delete_audit_report()` | `DELETE /pubapi/v1/audit/{type}/{id}` | `egnyte-client audit delete TYPE REPORT_ID` |
530
+ | `stream_audit_events()` | `POST /pubapi/v2/audit/stream` | `egnyte-client audit stream --start-date DATE` |
531
+
532
+ Reports are generated asynchronously: create one, poll `get_audit_job()` (Egnyte asks for no more than once every 2 minutes) until `is_complete`, then read it with `get_audit_report()` (JSON) or `download_audit_report()` (CSV bytes). `stream_audit_events()` pages through the last 7 days of events with a cursor.
533
+
534
+ ### Links
535
+
536
+ Docs: [Links](https://developers.egnyte.com/integration/cfs/api-docs/links-api)
537
+
538
+ | Client | Endpoint | CLI |
539
+ | --- | --- | --- |
540
+ | `list_links()` | `GET /pubapi/v2/links` | `egnyte-client links list` |
541
+ | `get_link()` | `GET /pubapi/v1/links/{id}` | `egnyte-client links get` |
542
+ | `create_link()` | `POST /pubapi/v1/links` | `egnyte-client links create` |
543
+ | `delete_link()` | `DELETE /pubapi/v1/links/{id}` | `egnyte-client links delete` |
544
+
545
+ ### Users
546
+
547
+ Docs: [User Management](https://developers.egnyte.com/integration/cfs/api-docs/user-management-api)
548
+
549
+ | Client | Endpoint | CLI |
550
+ | --- | --- | --- |
551
+ | `list_users()` | `GET /pubapi/v2/users` | `egnyte-client users list` |
552
+ | `get_user()` | `GET /pubapi/v2/users/{id}` | `egnyte-client users get` |
553
+ | `create_user()` | `POST /pubapi/v2/users` | `egnyte-client users create` |
554
+ | `update_user()` | `PATCH /pubapi/v2/users/{id}` | `egnyte-client users update` |
555
+ | `delete_user()` | `DELETE /pubapi/v2/users/{id}` | `egnyte-client users delete` |
556
+
557
+ Creating a user emails an invitation unless `send_invite=False`. For `egnyte` authentication, Egnyte always sends one.
558
+
559
+ ### Groups
560
+
561
+ Docs: [Group Management](https://developers.egnyte.com/integration/cfs/api-docs/group-management)
562
+
563
+ | Client | Endpoint | CLI |
564
+ | --- | --- | --- |
565
+ | `list_groups()` | `GET /pubapi/v2/groups` | `egnyte-client groups list` |
566
+ | `get_group()` | `GET /pubapi/v2/groups/{id}` | `egnyte-client groups get` |
567
+ | `create_group()` | `POST /pubapi/v2/groups` | `egnyte-client groups create` |
568
+ | `update_group()` | `PATCH /pubapi/v2/groups/{id}` | `egnyte-client groups update` |
569
+ | `replace_group()` | `PUT /pubapi/v2/groups/{id}` | `egnyte-client groups replace ID NAME --member ID...` |
570
+ | `delete_group()` | `DELETE /pubapi/v2/groups/{id}` | `egnyte-client groups delete` |
571
+
572
+ Egnyte requires at least one member to create a group, although its documentation marks `members` optional.
573
+
574
+ ## License
575
+
576
+ [MIT](LICENSE)