nextcloud-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.
nextcloud_cli/utils.py ADDED
@@ -0,0 +1,129 @@
1
+ """Small helpers shared across command modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import sys
8
+ from collections.abc import Iterator
9
+ from contextlib import contextmanager, nullcontext
10
+ from datetime import UTC, datetime
11
+ from typing import Any, NoReturn
12
+
13
+ import click
14
+ from dateutil import parser as date_parser
15
+ from rich.console import Console
16
+ from rich.logging import RichHandler
17
+
18
+ CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
19
+
20
+ console = Console()
21
+ err_console = Console(stderr=True)
22
+
23
+
24
+ def configure_logging(verbosity: int) -> None:
25
+ """Configure stderr logging based on a -v / -vv counter.
26
+
27
+ - 0: WARNING (default — silent on success)
28
+ - 1: INFO (one line per HTTP request)
29
+ - 2+: DEBUG (full request/response headers via httpx)
30
+ """
31
+ if verbosity <= 0:
32
+ level = logging.WARNING
33
+ elif verbosity == 1:
34
+ level = logging.INFO
35
+ else:
36
+ level = logging.DEBUG
37
+
38
+ handler: logging.Handler
39
+ if err_console.is_terminal:
40
+ handler = RichHandler(
41
+ console=err_console,
42
+ show_path=False,
43
+ markup=False,
44
+ rich_tracebacks=True,
45
+ log_time_format="[%X]",
46
+ )
47
+ fmt = "%(name)s: %(message)s"
48
+ else:
49
+ handler = logging.StreamHandler(sys.stderr)
50
+ fmt = "%(asctime)s %(levelname)s %(name)s: %(message)s"
51
+ handler.setFormatter(logging.Formatter(fmt))
52
+ logging.basicConfig(level=level, handlers=[handler], force=True)
53
+ if verbosity < 2:
54
+ logging.getLogger("httpcore").setLevel(logging.WARNING)
55
+
56
+
57
+ def _verbose_callback(ctx: click.Context, param: click.Parameter, value: int) -> int:
58
+ if value:
59
+ configure_logging(value)
60
+ return value
61
+
62
+
63
+ verbose_option = click.option(
64
+ "-v",
65
+ "--verbose",
66
+ count=True,
67
+ expose_value=False,
68
+ is_eager=True,
69
+ callback=_verbose_callback,
70
+ help="Increase log verbosity (-v: HTTP requests, -vv: full headers).",
71
+ )
72
+
73
+ json_option = click.option(
74
+ "--json",
75
+ "json_output",
76
+ is_flag=True,
77
+ default=False,
78
+ help="Emit raw JSON to stdout (machine-readable, no spinner, no colors).",
79
+ )
80
+
81
+
82
+ def parse_datetime(value: str, default_tz: str = "UTC") -> datetime:
83
+ """Parse an ISO 8601 string into an aware datetime."""
84
+ dt = date_parser.isoparse(value)
85
+ if dt.tzinfo is None:
86
+ try:
87
+ from zoneinfo import ZoneInfo
88
+
89
+ dt = dt.replace(tzinfo=ZoneInfo(default_tz))
90
+ except Exception:
91
+ dt = dt.replace(tzinfo=UTC)
92
+ return dt
93
+
94
+
95
+ def format_size(num_bytes: int) -> str:
96
+ size = float(num_bytes)
97
+ for unit in ("B", "KB", "MB", "GB", "TB"):
98
+ if size < 1024 or unit == "TB":
99
+ return f"{size:.1f} {unit}" if unit != "B" else f"{int(size)} B"
100
+ size /= 1024
101
+ return f"{size:.1f} TB"
102
+
103
+
104
+ @contextmanager
105
+ def spinner(message: str, json_output: bool = False) -> Iterator[None]:
106
+ """Show a Rich spinner on stderr while a block runs.
107
+
108
+ Suppressed in --json mode and when stderr is not a TTY (CI logs stay clean).
109
+ """
110
+ if json_output or not err_console.is_terminal:
111
+ with nullcontext():
112
+ yield
113
+ return
114
+ with err_console.status(f"[bold cyan]{message}[/bold cyan]", spinner="dots"):
115
+ yield
116
+
117
+
118
+ def emit(payload: Any) -> None:
119
+ """Print JSON to stdout. Used in --json mode and as a fallback."""
120
+ json.dump(payload, sys.stdout, indent=2, default=str, ensure_ascii=False)
121
+ sys.stdout.write("\n")
122
+
123
+
124
+ def fail(message: str, code: int = 1) -> NoReturn:
125
+ if err_console.is_terminal:
126
+ err_console.print(f"[bold red]error:[/bold red] {message}")
127
+ else:
128
+ sys.stderr.write(f"error: {message}\n")
129
+ sys.exit(code)
@@ -0,0 +1,282 @@
1
+ Metadata-Version: 2.4
2
+ Name: nextcloud-cli
3
+ Version: 0.1.0
4
+ Summary: Modern command-line client for Nextcloud (files, notes, calendar, tasks, contacts)
5
+ Project-URL: Homepage, https://github.com/AlexMili/nextcloud-cli
6
+ Project-URL: Issues, https://github.com/AlexMili/nextcloud-cli/issues
7
+ Project-URL: Source, https://github.com/AlexMili/nextcloud-cli
8
+ Author: AlexMili
9
+ License-File: LICENSE
10
+ Keywords: caldav,carddav,cli,nextcloud,webdav
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: End Users/Desktop
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Communications :: File Sharing
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.9
24
+ Requires-Dist: caldav>=1.3
25
+ Requires-Dist: click>=8.1
26
+ Requires-Dist: httpx>=0.27
27
+ Requires-Dist: icalendar>=5.0
28
+ Requires-Dist: keyring>=24.0
29
+ Requires-Dist: python-dateutil>=2.8
30
+ Requires-Dist: rich>=13.0
31
+ Requires-Dist: tzlocal>=5.0
32
+ Requires-Dist: vobject>=0.9.7
33
+ Requires-Dist: webdav4>=0.10
34
+ Provides-Extra: dev
35
+ Requires-Dist: mypy>=1.0; extra == 'dev'
36
+ Requires-Dist: pytest-mock>=3.10; extra == 'dev'
37
+ Requires-Dist: pytest>=7.0; extra == 'dev'
38
+ Requires-Dist: ruff>=0.4; extra == 'dev'
39
+ Description-Content-Type: text/markdown
40
+
41
+ # nextcloud-cli
42
+
43
+ `nxcloud` is a modern command-line client for self-hosted Nextcloud. Manage
44
+ files, notes, calendar events, tasks, and contacts from the terminal — over
45
+ HTTPS only, no desktop sync client required.
46
+
47
+ It speaks WebDAV via [`webdav4`](https://pypi.org/project/webdav4/), CalDAV
48
+ via [`caldav`](https://pypi.org/project/caldav/), CardDAV via raw `httpx` +
49
+ [`vobject`](https://pypi.org/project/vobject/), and the Nextcloud Notes REST
50
+ API via `httpx`. Credentials are stored in your OS keyring.
51
+
52
+ ## Features
53
+
54
+ - **Files** — list, upload, download, move, delete, mkdir, recursive search
55
+ - **Notes** — create, read, update, delete (Nextcloud Notes app required)
56
+ - **Calendar** — list calendars, list/create/edit/delete events, **with attendees and organizer**
57
+ - **Tasks** — list/create/complete/edit/delete VTODO items
58
+ - **Contacts** — list address books, list/get/export vCards
59
+ - **Login wizard** that validates the credentials with a `PROPFIND` and stores the password in the OS keyring (with a `chmod 0600` JSON fallback)
60
+ - **JSON output everywhere** so you can pipe into `jq` and friends
61
+ - `-h` / `--help` available on every command and subcommand
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ pip install nextcloud-cli
67
+ ```
68
+
69
+ Or from source:
70
+
71
+ ```bash
72
+ git clone https://github.com/AlexMili/nextcloud-cli
73
+ cd nextcloud-cli
74
+ pip install -e .
75
+ ```
76
+
77
+ The installed binary is **`nxcloud`**.
78
+
79
+ ## Login
80
+
81
+ Create a Nextcloud **app password** first
82
+ (`Settings → Security → App passwords`), then:
83
+
84
+ ```bash
85
+ nxcloud login
86
+ ```
87
+
88
+ You'll only be prompted for what's missing. Values are resolved in this order:
89
+
90
+ 1. CLI flag (`--url`, `--username`, `--password`, `--timezone`)
91
+ 2. Environment variable (`NEXTCLOUD_URL`, `NEXTCLOUD_USER`, `NEXTCLOUD_TOKEN`, `NEXTCLOUD_TIMEZONE`)
92
+ 3. Interactive prompt
93
+
94
+ So all of these work:
95
+
96
+ ```bash
97
+ # Fully interactive
98
+ nxcloud login
99
+
100
+ # Mixed: URL from env, prompt for the rest
101
+ NEXTCLOUD_URL=https://nc.example.com nxcloud login
102
+
103
+ # Fully non-interactive (no prompts)
104
+ nxcloud login \
105
+ --url https://nc.example.com \
106
+ --username alice \
107
+ --password 'app-pass-here' \
108
+ --timezone Europe/Paris
109
+ ```
110
+
111
+ You can also skip persistence entirely and rely on environment variables for
112
+ every command:
113
+
114
+ ```bash
115
+ export NEXTCLOUD_URL=https://nc.example.com
116
+ export NEXTCLOUD_USER=alice
117
+ export NEXTCLOUD_TOKEN=app-pass-here
118
+ export NEXTCLOUD_TIMEZONE=Europe/Paris
119
+ nxcloud check
120
+ ```
121
+
122
+ To remove stored credentials:
123
+
124
+ ```bash
125
+ nxcloud logout
126
+ ```
127
+
128
+ ## Usage
129
+
130
+ Every command emits JSON on stdout for easy scripting. Use `-h` on any
131
+ command or subcommand for built-in help.
132
+
133
+ ### Connectivity check
134
+
135
+ ```bash
136
+ nxcloud check
137
+ ```
138
+
139
+ ### Files
140
+
141
+ ```bash
142
+ nxcloud files list --path /Documents
143
+ nxcloud files upload --local ./report.pdf --remote /Documents/report.pdf
144
+ nxcloud files download --remote /Documents/report.pdf --local ./report.pdf
145
+ nxcloud files move --src /tmp/a.txt --dst /archive/a.txt
146
+ nxcloud files mkdir --path /new-folder
147
+ nxcloud files delete --path /old.txt
148
+ nxcloud files search --query report --limit 20 # OCS unified search (server-side)
149
+ ```
150
+
151
+ ### Notes (requires the Nextcloud Notes app)
152
+
153
+ ```bash
154
+ nxcloud notes list
155
+ nxcloud notes list --category Work # notes in the "Work" folder
156
+ nxcloud notes list --category "Work/Projects" # nested folder (subdirectory)
157
+ nxcloud notes list --category="" # only notes at the Notes/ root (no category)
158
+ nxcloud notes list --limit 10 # cap the number of notes fetched
159
+ nxcloud notes get --id 941
160
+ nxcloud notes create --title "Meeting" --content "Q3 roadmap." --category Work
161
+ nxcloud notes edit --id 941 --title "Updated"
162
+ nxcloud notes delete --id 941
163
+ ```
164
+
165
+ > In the Nextcloud Notes app, **category = folder**: notes are stored as
166
+ > Markdown files under `Notes/<category>/`, and `/` separates subdirectories.
167
+ > `--category` matches **exactly** — `--category Work` does not include
168
+ > `Work/Projects`. Use `--category=""` to list only notes at the root.
169
+
170
+ ### Calendar
171
+
172
+ List calendars and events:
173
+
174
+ ```bash
175
+ nxcloud calendar list
176
+ nxcloud calendar events --calendar Personal --start 2026-05-01 --end 2026-05-31
177
+ ```
178
+
179
+ Date-range shortcuts (server-side filtering, in your configured timezone):
180
+
181
+ ```bash
182
+ nxcloud calendar events --calendar Personal --today
183
+ nxcloud calendar events --calendar Personal --this-week # current Mon → Mon
184
+ nxcloud calendar events --calendar Personal --next-week # upcoming Mon → Mon
185
+ nxcloud calendar events --calendar Personal --this-month # 1st → 1st of next month
186
+ nxcloud calendar events --calendar Personal --next-month
187
+ nxcloud calendar events --calendar Personal --next 7d # also: 48h, 2w
188
+ ```
189
+
190
+ > Shortcuts are mutually exclusive with each other and with `--start/--end`.
191
+
192
+ Create an event with attendees and an organizer:
193
+
194
+ ```bash
195
+ nxcloud calendar create \
196
+ --calendar Work \
197
+ --summary "Project sync" \
198
+ --start 2026-07-01T14:00:00 \
199
+ --end 2026-07-01T15:00:00 \
200
+ --location "Room 4B" \
201
+ --description "Quarterly review" \
202
+ --organizer "Alice <alice@example.com>" \
203
+ --attendee "Bob <bob@example.com>" \
204
+ --attendee carol@example.com
205
+ ```
206
+
207
+ `--attendee` is repeatable and accepts either a bare email or the
208
+ `Name <email>` form.
209
+
210
+ Edit an event (including adding/removing invitees):
211
+
212
+ ```bash
213
+ nxcloud calendar edit --calendar Work --uid <uid> --summary "New title"
214
+ nxcloud calendar edit --calendar Work --uid <uid> \
215
+ --add-attendee "Dan <dan@example.com>" \
216
+ --remove-attendee carol@example.com
217
+ ```
218
+
219
+ Delete an event:
220
+
221
+ ```bash
222
+ nxcloud calendar delete --calendar Work --uid <uid>
223
+ ```
224
+
225
+ Search events server-side (CalDAV `text-match`). Combine with date filters:
226
+
227
+ ```bash
228
+ nxcloud calendar search --calendar Work --query standup
229
+ nxcloud calendar search --calendar Work --query roadmap --in description
230
+ nxcloud calendar search --calendar Work --query "1:1" --this-week
231
+ # --in: summary | description | location | category | all (default: summary)
232
+ ```
233
+
234
+ ### Tasks (VTODO)
235
+
236
+ ```bash
237
+ nxcloud tasks list
238
+ nxcloud tasks list --include-completed
239
+ nxcloud tasks create --summary "Review PR" --due 2026-07-05 --priority 1
240
+ nxcloud tasks complete --uid <uid>
241
+ nxcloud tasks edit --uid <uid> --summary "Updated summary"
242
+ nxcloud tasks delete --uid <uid>
243
+ nxcloud tasks search --query "deploy" --include-completed
244
+ nxcloud tasks search --query "fix" --in description --list Work
245
+ # --in: summary | description | category | all (default: summary)
246
+ ```
247
+
248
+ ### Contacts
249
+
250
+ ```bash
251
+ nxcloud contacts list
252
+ nxcloud contacts cards --addressbook contacts
253
+ nxcloud contacts get --addressbook contacts --uid <uid>
254
+ nxcloud contacts export --addressbook contacts --uid <uid> --local ./alice.vcf
255
+ nxcloud contacts search --addressbook contacts --query alex
256
+ nxcloud contacts search --addressbook contacts --query "@example.com" --in email
257
+ # --in: name | email | phone | all (default: all)
258
+ ```
259
+
260
+ ## Configuration files
261
+
262
+ | File | Purpose | Permissions |
263
+ |------|---------|-------------|
264
+ | `~/.config/nextcloud-cli/config.json` | URL, username, timezone | `0600` |
265
+ | OS keyring (entry `nextcloud-cli`) | App password | OS-managed |
266
+ | `~/.config/nextcloud-cli/secrets.json` | Password fallback when no keyring backend is available | `0600` |
267
+
268
+ Override the config directory with the `NEXTCLOUD_CLI_HOME` environment variable.
269
+
270
+ ## Help
271
+
272
+ `-h` and `--help` are wired up at every level:
273
+
274
+ ```bash
275
+ nxcloud --help
276
+ nxcloud calendar --help
277
+ nxcloud calendar create --help
278
+ ```
279
+
280
+ ## License
281
+
282
+ MIT
@@ -0,0 +1,21 @@
1
+ nextcloud_cli/VERSION.md,sha256=atlhOkVXmNbZLl9fOQq0uqcFlryGntaxf1zdKyhjXwY,5
2
+ nextcloud_cli/__init__.py,sha256=Uk7Cw4n-7jFgEGpixZL9VSOUspcLOvLv76HxxbnWAVc,71
3
+ nextcloud_cli/__main__.py,sha256=XCMQvGDl8AgHJwA5Q5xk2YUVnrKhgS0Oaf9MtW0p-gg,74
4
+ nextcloud_cli/cli.py,sha256=bnVKzg-z6wX94iVJjUsQ3PwNbyOCwhFw-P0zkZ_oX7s,985
5
+ nextcloud_cli/client.py,sha256=4utyvj6__GpR4jPthgk2Ps599U0OeR7p8O25Eqep24A,1241
6
+ nextcloud_cli/config.py,sha256=iCqF-YEnvZhfNI6K0JdoZ_eIYqmfQ42_dgQ4cOM02YM,4544
7
+ nextcloud_cli/rendering.py,sha256=tJvULiTE81mdwspyCQIhgwMAlw9tMhljEdYK8qVTTWw,9751
8
+ nextcloud_cli/utils.py,sha256=NwTmN9p_UGFvErM6qLJnpAvR9l1vRVZOTijXXyM_ysE,3689
9
+ nextcloud_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ nextcloud_cli/commands/calendar.py,sha256=NLXSNxeSThpYtJDR9Flwdt3BR4o15pKt0oHrcUYyWuQ,15769
11
+ nextcloud_cli/commands/check.py,sha256=11BGhR6Q3i0BFcIcLqfRexmLBVtnyFuuIsmI2Jshvjs,2480
12
+ nextcloud_cli/commands/contacts.py,sha256=hvLt4PLwNhA_am6TpsSgoBSDPzzjMtKKsgynI66h-yA,9014
13
+ nextcloud_cli/commands/files.py,sha256=Rh0x83m3e7POZeJbWbDOP_kIAhagbnIQ-MX6XNsDzQs,5740
14
+ nextcloud_cli/commands/notes.py,sha256=G-tGE-Nyi3ZoIkPiQdehcezSxE97tDDf7W0LtfFWTvE,4370
15
+ nextcloud_cli/commands/setup.py,sha256=kNH_5bQHZbR1w9YBRQ0SFhzkL8McBZyTsooExOhWYcg,2658
16
+ nextcloud_cli/commands/tasks.py,sha256=ha47kpJ2InkS_jT0mTNDpdg7qahZ4Y1iqB9ytb16Q5g,8528
17
+ nextcloud_cli-0.1.0.dist-info/METADATA,sha256=nCvZrztoQsXKYRsMEoxO-XrqDwCym791Wt08lBm416w,8928
18
+ nextcloud_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
19
+ nextcloud_cli-0.1.0.dist-info/entry_points.txt,sha256=fmuw7wYIkiG9hJRo8ScqGbCePp0vSreWlsA73SZitHg,51
20
+ nextcloud_cli-0.1.0.dist-info/licenses/LICENSE,sha256=M5XFEFVqTKWMabj00jE2NKZen6Ri6fFdh9NMOiBV0AQ,1066
21
+ nextcloud_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ nxcloud = nextcloud_cli.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexandre
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.