indico-cli 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 (28) hide show
  1. indico_cli-0.1.0/.gitignore +11 -0
  2. indico_cli-0.1.0/.gitlab-ci.yml +15 -0
  3. indico_cli-0.1.0/CLAUDE.md +36 -0
  4. indico_cli-0.1.0/LICENSE +21 -0
  5. indico_cli-0.1.0/PKG-INFO +118 -0
  6. indico_cli-0.1.0/README.md +98 -0
  7. indico_cli-0.1.0/indico_cli/__init__.py +3 -0
  8. indico_cli-0.1.0/indico_cli/cli.py +326 -0
  9. indico_cli-0.1.0/indico_cli/indico_api.py +1766 -0
  10. indico_cli-0.1.0/indico_cli/utils.py +145 -0
  11. indico_cli-0.1.0/pyproject.toml +28 -0
  12. indico_cli-0.1.0/skill/.claude-plugin/plugin.json +11 -0
  13. indico_cli-0.1.0/skill/package.json +7 -0
  14. indico_cli-0.1.0/skill/skills/add-timetable-entry/SKILL.md +41 -0
  15. indico_cli-0.1.0/skill/skills/configure/SKILL.md +74 -0
  16. indico_cli-0.1.0/skill/skills/create-contribution/SKILL.md +48 -0
  17. indico_cli-0.1.0/skill/skills/create-event/SKILL.md +52 -0
  18. indico_cli-0.1.0/skill/skills/create-event-with-agenda/SKILL.md +74 -0
  19. indico_cli-0.1.0/skill/skills/create-session/SKILL.md +33 -0
  20. indico_cli-0.1.0/skill/skills/delete-timetable-entry/SKILL.md +32 -0
  21. indico_cli-0.1.0/skill/skills/download-attachment/SKILL.md +38 -0
  22. indico_cli-0.1.0/skill/skills/get-contributions/SKILL.md +39 -0
  23. indico_cli-0.1.0/skill/skills/get-event-details/SKILL.md +39 -0
  24. indico_cli-0.1.0/skill/skills/get-files/SKILL.md +54 -0
  25. indico_cli-0.1.0/skill/skills/get-user-info/SKILL.md +34 -0
  26. indico_cli-0.1.0/skill/skills/search-by-term/SKILL.md +38 -0
  27. indico_cli-0.1.0/skill/skills/search-categories/SKILL.md +38 -0
  28. indico_cli-0.1.0/skill/skills/search-events/SKILL.md +41 -0
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ downloads/
7
+ *.pdf
8
+ *.pptx
9
+ skill/indico-cli-workspace/
10
+ .env
11
+ .env.*
@@ -0,0 +1,15 @@
1
+ stages:
2
+ - publish
3
+
4
+ publish-pypi:
5
+ stage: publish
6
+ image: python:3.12-slim
7
+ rules:
8
+ - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+/
9
+ script:
10
+ - pip install build twine
11
+ - python -m build
12
+ - twine upload dist/*
13
+ variables:
14
+ TWINE_USERNAME: __token__
15
+ TWINE_PASSWORD: $PYPI_API_TOKEN
@@ -0,0 +1,36 @@
1
+ # CLAUDE.md
2
+
3
+ ## Project Overview
4
+
5
+ Python CLI tool for interacting with Indico event management systems. Extracted from the `indico-mcp` MCP server. Run via `uvx indico-cli` or install with `pip install indico-cli`.
6
+
7
+ ## Architecture
8
+
9
+ - `indico_cli/cli.py`: Click CLI entry point with 15 subcommands
10
+ - `indico_cli/indico_api.py`: HTTP API client (`IndicoAPI` for low-level requests, `IndicoClient` for high-level operations)
11
+ - `indico_cli/utils.py`: Utility functions for building Indico API request data
12
+ - `skill/`: Claude Code skill package (14 skills mapping to CLI commands)
13
+
14
+ ## Development
15
+
16
+ ```bash
17
+ # Install in dev mode
18
+ uv pip install -e .
19
+
20
+ # Run CLI
21
+ indico-cli --help
22
+
23
+ # Run with uvx from source
24
+ uvx --from . indico-cli --help
25
+ ```
26
+
27
+ ## Configuration
28
+
29
+ Requires env vars `INDICO_BASE_URL` and `INDICO_API_TOKEN`, or use `indico-cli configure`. A `.env` file is supported but must never be committed.
30
+
31
+ ## Key Technical Details
32
+
33
+ - All API calls are async (httpx). The CLI wraps them with `asyncio.run()`.
34
+ - Bearer token auth only (tokens starting with `indp_`). No `Content-Type` header on GET requests (Indico returns 403 for file downloads if `Content-Type: application/json` is set).
35
+ - The Indico export API returns local sequential contribution IDs (e.g., 1, 2, 3) not database IDs. Always use `download_url` from API responses for file downloads, not reconstructed paths.
36
+ - Room booking is excluded (needs separate fix).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 indico-cli contributors
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,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: indico-cli
3
+ Version: 0.1.0
4
+ Summary: CLI tool for interacting with Indico event management systems
5
+ Project-URL: Repository, https://gitlab.cern.ch/cern-agent-skills/indico-cli
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: cern,cli,conferences,events,indico
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Scientific/Engineering
16
+ Requires-Python: >=3.12
17
+ Requires-Dist: click>=8.0
18
+ Requires-Dist: httpx>=0.25
19
+ Description-Content-Type: text/markdown
20
+
21
+ # indico-cli
22
+
23
+ Command line interface for interacting with [Indico](https://getindico.io/) event management systems.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ # Run directly with uvx (recommended)
29
+ uvx indico-cli --help
30
+
31
+ # Or install with pip
32
+ pip install indico-cli
33
+ ```
34
+
35
+ ## Configuration
36
+
37
+ Set environment variables:
38
+
39
+ ```bash
40
+ export INDICO_BASE_URL="https://indico.cern.ch"
41
+ export INDICO_API_TOKEN="indp_..." # Bearer token from Indico preferences
42
+ ```
43
+
44
+ Or use the `configure` command to save credentials:
45
+
46
+ ```bash
47
+ indico-cli configure --base-url https://indico.cern.ch --token indp_...
48
+ ```
49
+
50
+ API tokens can be generated at `<your-indico-url>/user/preferences/api`. Required scopes: `read:legacy_api`, `write:legacy_api` (for create operations), `read:user` (optional).
51
+
52
+ ## Usage
53
+
54
+ ### Search
55
+
56
+ ```bash
57
+ # Full-text search across all content types
58
+ indico-cli search-by-term --term "machine learning"
59
+
60
+ # Search events in a category
61
+ indico-cli search-events --category-id 4648 --from-date 2025-01-01
62
+
63
+ # Browse categories
64
+ indico-cli search-categories --category-id 0
65
+ ```
66
+
67
+ ### Event Details
68
+
69
+ ```bash
70
+ # Get event info with contributions and sessions
71
+ indico-cli event-details --event-id 137346 --contributions --sessions
72
+
73
+ # List contributions/talks
74
+ indico-cli contributions --event-id 137346 --subcontributions
75
+ ```
76
+
77
+ ### Files
78
+
79
+ ```bash
80
+ # List and download all event files
81
+ indico-cli files --event-id 137346
82
+
83
+ # List files without downloading
84
+ indico-cli files --event-id 137346 --no-download
85
+
86
+ # Download a single attachment
87
+ indico-cli download --url "https://indico.cern.ch/event/.../file.pdf"
88
+ ```
89
+
90
+ ### Create Events
91
+
92
+ ```bash
93
+ # Create a simple meeting
94
+ indico-cli create-event --title "Team Meeting" --category-id 4648 --type meeting \
95
+ --start 2025-06-15T09:00:00 --end 2025-06-15T10:00:00
96
+
97
+ # Create event with full agenda from JSON file
98
+ indico-cli create-event-with-agenda --title "Workshop" --category-id 4648 \
99
+ --type meeting --start 2025-06-15T09:00:00 --end 2025-06-15T17:00:00 \
100
+ --agenda-file agenda.json
101
+ ```
102
+
103
+ ### All Commands
104
+
105
+ Run `indico-cli --help` to see all available commands, or `indico-cli <command> --help` for command-specific options.
106
+
107
+ ## Claude Code Skill
108
+
109
+ A Claude Code skill package is available in `skill/` for AI-assisted Indico workflows. Install it with:
110
+
111
+ ```bash
112
+ claude plugin install ./skill
113
+ ```
114
+
115
+ ## Documentation
116
+
117
+ - [Indico HTTP API docs](https://docs.getindico.io/en/stable/http-api/)
118
+ - [Indico platform](https://getindico.io/)
@@ -0,0 +1,98 @@
1
+ # indico-cli
2
+
3
+ Command line interface for interacting with [Indico](https://getindico.io/) event management systems.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Run directly with uvx (recommended)
9
+ uvx indico-cli --help
10
+
11
+ # Or install with pip
12
+ pip install indico-cli
13
+ ```
14
+
15
+ ## Configuration
16
+
17
+ Set environment variables:
18
+
19
+ ```bash
20
+ export INDICO_BASE_URL="https://indico.cern.ch"
21
+ export INDICO_API_TOKEN="indp_..." # Bearer token from Indico preferences
22
+ ```
23
+
24
+ Or use the `configure` command to save credentials:
25
+
26
+ ```bash
27
+ indico-cli configure --base-url https://indico.cern.ch --token indp_...
28
+ ```
29
+
30
+ API tokens can be generated at `<your-indico-url>/user/preferences/api`. Required scopes: `read:legacy_api`, `write:legacy_api` (for create operations), `read:user` (optional).
31
+
32
+ ## Usage
33
+
34
+ ### Search
35
+
36
+ ```bash
37
+ # Full-text search across all content types
38
+ indico-cli search-by-term --term "machine learning"
39
+
40
+ # Search events in a category
41
+ indico-cli search-events --category-id 4648 --from-date 2025-01-01
42
+
43
+ # Browse categories
44
+ indico-cli search-categories --category-id 0
45
+ ```
46
+
47
+ ### Event Details
48
+
49
+ ```bash
50
+ # Get event info with contributions and sessions
51
+ indico-cli event-details --event-id 137346 --contributions --sessions
52
+
53
+ # List contributions/talks
54
+ indico-cli contributions --event-id 137346 --subcontributions
55
+ ```
56
+
57
+ ### Files
58
+
59
+ ```bash
60
+ # List and download all event files
61
+ indico-cli files --event-id 137346
62
+
63
+ # List files without downloading
64
+ indico-cli files --event-id 137346 --no-download
65
+
66
+ # Download a single attachment
67
+ indico-cli download --url "https://indico.cern.ch/event/.../file.pdf"
68
+ ```
69
+
70
+ ### Create Events
71
+
72
+ ```bash
73
+ # Create a simple meeting
74
+ indico-cli create-event --title "Team Meeting" --category-id 4648 --type meeting \
75
+ --start 2025-06-15T09:00:00 --end 2025-06-15T10:00:00
76
+
77
+ # Create event with full agenda from JSON file
78
+ indico-cli create-event-with-agenda --title "Workshop" --category-id 4648 \
79
+ --type meeting --start 2025-06-15T09:00:00 --end 2025-06-15T17:00:00 \
80
+ --agenda-file agenda.json
81
+ ```
82
+
83
+ ### All Commands
84
+
85
+ Run `indico-cli --help` to see all available commands, or `indico-cli <command> --help` for command-specific options.
86
+
87
+ ## Claude Code Skill
88
+
89
+ A Claude Code skill package is available in `skill/` for AI-assisted Indico workflows. Install it with:
90
+
91
+ ```bash
92
+ claude plugin install ./skill
93
+ ```
94
+
95
+ ## Documentation
96
+
97
+ - [Indico HTTP API docs](https://docs.getindico.io/en/stable/http-api/)
98
+ - [Indico platform](https://getindico.io/)
@@ -0,0 +1,3 @@
1
+ """Indico CLI - Command line interface for Indico event management systems."""
2
+
3
+ __version__ = "0.1.0"
@@ -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()