indico-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.
indico_cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Indico CLI - Command line interface for Indico event management systems."""
2
+
3
+ __version__ = "0.1.0"
indico_cli/cli.py ADDED
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/env python3
2
+ """CLI entry point for interacting with Indico event management systems."""
3
+
4
+ import asyncio
5
+ import json
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ import click
12
+
13
+ from indico_cli.indico_api import IndicoAPI, IndicoClient
14
+
15
+ CONFIG_DIR = Path.home() / ".config" / "indico-cli"
16
+ CONFIG_FILE = CONFIG_DIR / "config.json"
17
+
18
+
19
+ def load_config() -> dict:
20
+ """Load saved configuration from disk, returning empty dict on failure."""
21
+ if CONFIG_FILE.exists():
22
+ try:
23
+ return json.loads(CONFIG_FILE.read_text())
24
+ except (json.JSONDecodeError, OSError):
25
+ return {}
26
+ return {}
27
+
28
+
29
+ def save_config(base_url: str, token: str) -> None:
30
+ """Persist configuration to ~/.config/indico-cli/config.json."""
31
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
32
+ CONFIG_FILE.write_text(json.dumps({"base_url": base_url, "token": token}, indent=2))
33
+
34
+
35
+ def get_client(base_url: Optional[str], token: Optional[str]) -> IndicoClient:
36
+ """Create an IndicoClient from CLI args, env vars, or saved config."""
37
+ saved_config = load_config()
38
+ resolved_url = base_url or os.getenv("INDICO_BASE_URL") or saved_config.get("base_url", "")
39
+ resolved_token = token or os.getenv("INDICO_API_TOKEN") or saved_config.get("token", "")
40
+
41
+ if not resolved_url:
42
+ click.echo("Error: --base-url, INDICO_BASE_URL, or saved config required", err=True)
43
+ sys.exit(1)
44
+
45
+ api = IndicoAPI(resolved_url, bearer_token=resolved_token if resolved_token else None)
46
+ return IndicoClient(api)
47
+
48
+
49
+ def run_async(coro):
50
+ """Run an async coroutine and return its result."""
51
+ return asyncio.run(coro)
52
+
53
+
54
+ def parse_speakers_json(speakers_string: Optional[str]):
55
+ """Parse a JSON string of speakers into a list of dicts."""
56
+ if not speakers_string:
57
+ return None
58
+ return json.loads(speakers_string)
59
+
60
+
61
+ @click.group()
62
+ @click.option("--base-url", envvar="INDICO_BASE_URL", help="Indico instance URL")
63
+ @click.option("--token", envvar="INDICO_API_TOKEN", help="API bearer token")
64
+ @click.pass_context
65
+ def cli(ctx, base_url, token):
66
+ """CLI for interacting with Indico event management systems."""
67
+ ctx.ensure_object(dict)
68
+ ctx.obj["base_url"] = base_url
69
+ ctx.obj["token"] = token
70
+
71
+
72
+ @cli.command()
73
+ @click.option("--base-url", required=True, help="Indico instance URL")
74
+ @click.option("--token", default="", help="API bearer token")
75
+ def configure(base_url, token):
76
+ """Configure connection to an Indico instance and test it."""
77
+ api = IndicoAPI(base_url, bearer_token=token if token else None)
78
+ client = IndicoClient(api)
79
+ try:
80
+ result = run_async(client.get_user_info())
81
+ click.echo("Connection successful!")
82
+ click.echo(result)
83
+ save_config(base_url, token)
84
+ click.echo(f"Configuration saved to {CONFIG_FILE}")
85
+ except Exception as error:
86
+ click.echo(f"Connection test failed: {error}", err=True)
87
+ sys.exit(1)
88
+
89
+
90
+ @cli.command("user-info")
91
+ @click.pass_context
92
+ def user_info(ctx):
93
+ """Get current user information."""
94
+ client = get_client(ctx.obj["base_url"], ctx.obj["token"])
95
+ result = run_async(client.get_user_info())
96
+ click.echo(result)
97
+
98
+
99
+ @cli.command("search-events")
100
+ @click.option("--category-id", required=True, help="Category ID to search in")
101
+ @click.option("--from-date", default=None, help="Start date filter (e.g. 2024-01-01)")
102
+ @click.option("--to-date", default=None, help="End date filter (e.g. 2024-12-31)")
103
+ @click.option("--limit", type=int, default=None, help="Maximum number of results")
104
+ @click.option("--only-public", is_flag=True, default=False, help="Only show public events")
105
+ @click.pass_context
106
+ def search_events(ctx, category_id, from_date, to_date, limit, only_public):
107
+ """Search for events in a category."""
108
+ client = get_client(ctx.obj["base_url"], ctx.obj["token"])
109
+ result = run_async(client.search_events(category_id, from_date, to_date, limit, only_public))
110
+ click.echo(result)
111
+
112
+
113
+ @cli.command("search-by-term")
114
+ @click.option("--term", required=True, help="Search term")
115
+ @click.option("--limit", type=int, default=50, help="Maximum number of results")
116
+ @click.pass_context
117
+ def search_by_term(ctx, term, limit):
118
+ """Search for events by keyword/term."""
119
+ client = get_client(ctx.obj["base_url"], ctx.obj["token"])
120
+ result = run_async(client.search_events_by_term(term, limit))
121
+ click.echo(result)
122
+
123
+
124
+ @cli.command("search-categories")
125
+ @click.option("--category-id", default="0", help="Parent category ID (default: root)")
126
+ @click.option("--limit", type=int, default=None, help="Maximum number of results")
127
+ @click.pass_context
128
+ def search_categories(ctx, category_id, limit):
129
+ """Search for event categories."""
130
+ client = get_client(ctx.obj["base_url"], ctx.obj["token"])
131
+ result = run_async(client.search_categories(category_id, limit))
132
+ click.echo(result)
133
+
134
+
135
+ @cli.command("event-details")
136
+ @click.option("--event-id", required=True, help="Event ID")
137
+ @click.option("--contributions", is_flag=True, default=False, help="Include contributions")
138
+ @click.option("--sessions", is_flag=True, default=False, help="Include sessions")
139
+ @click.pass_context
140
+ def event_details(ctx, event_id, contributions, sessions):
141
+ """Get detailed information about a specific event."""
142
+ client = get_client(ctx.obj["base_url"], ctx.obj["token"])
143
+ result = run_async(client.get_event_details(event_id, contributions, sessions))
144
+ click.echo(result)
145
+
146
+
147
+ @cli.command()
148
+ @click.option("--event-id", required=True, help="Event ID")
149
+ @click.option("--subcontributions", is_flag=True, default=False, help="Include subcontributions")
150
+ @click.option("--limit", type=int, default=None, help="Maximum number of results")
151
+ @click.pass_context
152
+ def contributions(ctx, event_id, subcontributions, limit):
153
+ """Get contributions for a specific event."""
154
+ client = get_client(ctx.obj["base_url"], ctx.obj["token"])
155
+ result = run_async(client.get_event_contributions(event_id, subcontributions, limit))
156
+ click.echo(result)
157
+
158
+
159
+ @cli.command()
160
+ @click.option("--event-id", required=True, help="Event ID")
161
+ @click.option("--download/--no-download", default=True, help="Download files (default: yes)")
162
+ @click.option("--download-dir", default=None, help="Directory to save downloaded files")
163
+ @click.pass_context
164
+ def files(ctx, event_id, download, download_dir):
165
+ """Get and optionally download files for an event."""
166
+ client = get_client(ctx.obj["base_url"], ctx.obj["token"])
167
+ result = run_async(client.get_files(event_id, download_files=download, download_dir=download_dir))
168
+ click.echo(result)
169
+
170
+
171
+ @cli.command()
172
+ @click.option("--url", required=True, help="Download URL for the attachment")
173
+ @click.option("--filename", default=None, help="Save as this filename")
174
+ @click.pass_context
175
+ def download(ctx, url, filename):
176
+ """Download a single file attachment."""
177
+ client = get_client(ctx.obj["base_url"], ctx.obj["token"])
178
+ result = run_async(client.download_single_attachment(url, filename))
179
+ click.echo(result)
180
+
181
+
182
+ @cli.command("create-event")
183
+ @click.option("--title", required=True, help="Event title")
184
+ @click.option("--category-id", required=True, help="Category ID")
185
+ @click.option("--type", "event_type", required=True, type=click.Choice(["lecture", "meeting", "conference"]), help="Event type")
186
+ @click.option("--start", default=None, help="Start date/time (ISO format)")
187
+ @click.option("--end", default=None, help="End date/time (ISO format)")
188
+ @click.option("--timezone", default=None, help="Timezone (e.g. Europe/Zurich)")
189
+ @click.option("--description", default=None, help="Event description")
190
+ @click.option("--location", default=None, help="Event location")
191
+ @click.option("--room", default=None, help="Room name")
192
+ @click.option("--speakers", default=None, help="Speakers as JSON string")
193
+ @click.pass_context
194
+ def create_event(ctx, title, category_id, event_type, start, end, timezone, description, location, room, speakers):
195
+ """Create a new event."""
196
+ client = get_client(ctx.obj["base_url"], ctx.obj["token"])
197
+ parsed_speakers = parse_speakers_json(speakers)
198
+ result = run_async(client.create_event(
199
+ title=title,
200
+ category_id=category_id,
201
+ event_type=event_type,
202
+ start_date=start,
203
+ end_date=end,
204
+ timezone=timezone,
205
+ description=description,
206
+ location=location,
207
+ room=room,
208
+ speakers=parsed_speakers,
209
+ ))
210
+ click.echo(result)
211
+
212
+
213
+ @cli.command("create-session")
214
+ @click.option("--event-id", required=True, help="Event ID")
215
+ @click.option("--title", required=True, help="Session title")
216
+ @click.option("--description", default=None, help="Session description")
217
+ @click.pass_context
218
+ def create_session(ctx, event_id, title, description):
219
+ """Create a session in an event."""
220
+ client = get_client(ctx.obj["base_url"], ctx.obj["token"])
221
+ result = run_async(client.create_session(
222
+ event_id=event_id,
223
+ title=title,
224
+ description=description,
225
+ ))
226
+ click.echo(result)
227
+
228
+
229
+ @cli.command("create-contribution")
230
+ @click.option("--event-id", required=True, help="Event ID")
231
+ @click.option("--title", required=True, help="Contribution title")
232
+ @click.option("--description", default=None, help="Contribution description")
233
+ @click.option("--duration", type=int, default=None, help="Duration in minutes")
234
+ @click.option("--speakers", default=None, help="Speakers as JSON string")
235
+ @click.option("--session-id", default=None, help="Session ID to add contribution to")
236
+ @click.pass_context
237
+ def create_contribution(ctx, event_id, title, description, duration, speakers, session_id):
238
+ """Create a contribution in an event."""
239
+ client = get_client(ctx.obj["base_url"], ctx.obj["token"])
240
+ parsed_speakers = parse_speakers_json(speakers)
241
+ result = run_async(client.create_contribution(
242
+ event_id=event_id,
243
+ title=title,
244
+ description=description,
245
+ duration_minutes=duration,
246
+ speakers=parsed_speakers,
247
+ session_id=session_id,
248
+ ))
249
+ click.echo(result)
250
+
251
+
252
+ @cli.command("add-timetable-entry")
253
+ @click.option("--event-id", required=True, help="Event ID")
254
+ @click.option("--type", "entry_type", required=True, type=click.Choice(["session_block", "contribution"]), help="Entry type")
255
+ @click.option("--object-id", required=True, help="Object ID (session or contribution)")
256
+ @click.option("--start", required=True, help="Start date/time (ISO format)")
257
+ @click.option("--duration", type=int, default=None, help="Duration in minutes")
258
+ @click.pass_context
259
+ def add_timetable_entry(ctx, event_id, entry_type, object_id, start, duration):
260
+ """Add an entry to the event timetable."""
261
+ client = get_client(ctx.obj["base_url"], ctx.obj["token"])
262
+ result = run_async(client.add_timetable_entry(
263
+ event_id=event_id,
264
+ entry_type=entry_type,
265
+ object_id=object_id,
266
+ start_date=start,
267
+ duration_minutes=duration,
268
+ ))
269
+ click.echo(result)
270
+
271
+
272
+ @cli.command("delete-timetable-entry")
273
+ @click.option("--event-id", required=True, help="Event ID")
274
+ @click.option("--entry-id", required=True, help="Timetable entry ID")
275
+ @click.pass_context
276
+ def delete_timetable_entry(ctx, event_id, entry_id):
277
+ """Delete a timetable entry from an event."""
278
+ client = get_client(ctx.obj["base_url"], ctx.obj["token"])
279
+ result = run_async(client.delete_timetable_entry(
280
+ event_id=event_id,
281
+ entry_id=entry_id,
282
+ ))
283
+ click.echo(result)
284
+
285
+
286
+ @cli.command("create-event-with-agenda")
287
+ @click.option("--title", required=True, help="Event title")
288
+ @click.option("--category-id", required=True, help="Category ID")
289
+ @click.option("--type", "event_type", required=True, type=click.Choice(["lecture", "meeting", "conference"]), help="Event type")
290
+ @click.option("--start", required=True, help="Start date/time (ISO format)")
291
+ @click.option("--end", required=True, help="End date/time (ISO format)")
292
+ @click.option("--timezone", default=None, help="Timezone (e.g. Europe/Zurich)")
293
+ @click.option("--description", default=None, help="Event description")
294
+ @click.option("--location", default=None, help="Event location")
295
+ @click.option("--room", default=None, help="Room name")
296
+ @click.option("--speakers", default=None, help="Speakers as JSON string")
297
+ @click.option("--agenda-file", type=click.Path(exists=True), default=None, help="Path to JSON file with agenda array")
298
+ @click.pass_context
299
+ def create_event_with_agenda(ctx, title, category_id, event_type, start, end, timezone, description, location, room, speakers, agenda_file):
300
+ """Create an event with a full agenda from a JSON file."""
301
+ client = get_client(ctx.obj["base_url"], ctx.obj["token"])
302
+ parsed_speakers = parse_speakers_json(speakers)
303
+
304
+ agenda_data = None
305
+ if agenda_file:
306
+ with open(agenda_file) as agenda_fh:
307
+ agenda_data = json.load(agenda_fh)
308
+
309
+ result = run_async(client.create_event_with_agenda(
310
+ title=title,
311
+ category_id=category_id,
312
+ event_type=event_type,
313
+ start_date=start,
314
+ end_date=end,
315
+ timezone=timezone,
316
+ description=description,
317
+ location=location,
318
+ room=room,
319
+ speakers=parsed_speakers,
320
+ agenda=agenda_data,
321
+ ))
322
+ click.echo(result)
323
+
324
+
325
+ if __name__ == "__main__":
326
+ cli()