indico-cli 0.4.1__tar.gz → 0.4.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: indico-cli
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: CLI tool for interacting with Indico event management systems
5
5
  Project-URL: Repository, https://gitlab.cern.ch/cern-agent-skills/indico-cli
6
6
  License: MIT
@@ -60,6 +60,9 @@ API tokens can be generated at `<your-indico-url>/user/preferences/api`. Require
60
60
  # Full-text search across all content types
61
61
  indico-cli search-by-term --term "machine learning"
62
62
 
63
+ # Full-text search with date range
64
+ indico-cli search-by-term --term "Smith" --type contribution --from-date 2025-01-01 --to-date 2025-03-31
65
+
63
66
  # Search events in a category
64
67
  indico-cli search-events --category-id 4648 --from-date 2025-01-01
65
68
 
@@ -90,6 +93,12 @@ indico-cli files --event-id 137346 --no-download
90
93
  indico-cli download --url "https://indico.cern.ch/event/.../file.pdf"
91
94
  ```
92
95
 
96
+ The `files` command covers every file source attached to an event:
97
+
98
+ - **Event materials** — legacy `event.material[]` entries.
99
+ - **Event folder attachments** — generic files uploaded directly to the event page (not under any contribution). Typical for short meetings like the LHC morning meeting, where a single slide deck is attached to the event itself.
100
+ - **Contribution attachments** — files attached to individual contributions/talks.
101
+
93
102
  ### Create Events
94
103
 
95
104
  ```bash
@@ -105,7 +114,25 @@ indico-cli create-event-with-agenda --title "Workshop" --category-id 4648 \
105
114
 
106
115
  ### All Commands
107
116
 
108
- Run `indico-cli --help` to see all available commands, or `indico-cli <command> --help` for command-specific options.
117
+ | Command | Description |
118
+ |---------|-------------|
119
+ | `configure` | Save Indico base URL and API token, and verify the connection. |
120
+ | `user-info` | Show the current user's name, email, and admin status. |
121
+ | `search-events` | List events in a category, with optional date range and public-only filter. |
122
+ | `search-by-term` | Full-text search across categories, events, contributions, attachments, and notes. |
123
+ | `search-categories` | Browse sub-categories of a given category. |
124
+ | `event-details` | Show event metadata, optionally with contributions and sessions. |
125
+ | `contributions` | List contributions (and optionally subcontributions) of an event. |
126
+ | `files` | List and download all files attached to an event — event-level materials, event-page folder attachments, and contribution attachments. |
127
+ | `download` | Download a single attachment by its URL. |
128
+ | `create-event` | Create a new event (lecture, meeting, or conference). |
129
+ | `create-session` | Create a session inside an event. |
130
+ | `create-contribution` | Create a contribution inside an event, optionally in a given session. |
131
+ | `add-timetable-entry` | Add a session block or contribution entry to the event timetable. |
132
+ | `delete-timetable-entry` | Remove an entry from the event timetable. |
133
+ | `create-event-with-agenda` | Create an event and populate its timetable from a JSON agenda file. |
134
+
135
+ Run `indico-cli --help` for the same list, or `indico-cli <command> --help` for command-specific options.
109
136
 
110
137
  ## Claude Code Skill
111
138
 
@@ -37,6 +37,9 @@ API tokens can be generated at `<your-indico-url>/user/preferences/api`. Require
37
37
  # Full-text search across all content types
38
38
  indico-cli search-by-term --term "machine learning"
39
39
 
40
+ # Full-text search with date range
41
+ indico-cli search-by-term --term "Smith" --type contribution --from-date 2025-01-01 --to-date 2025-03-31
42
+
40
43
  # Search events in a category
41
44
  indico-cli search-events --category-id 4648 --from-date 2025-01-01
42
45
 
@@ -67,6 +70,12 @@ indico-cli files --event-id 137346 --no-download
67
70
  indico-cli download --url "https://indico.cern.ch/event/.../file.pdf"
68
71
  ```
69
72
 
73
+ The `files` command covers every file source attached to an event:
74
+
75
+ - **Event materials** — legacy `event.material[]` entries.
76
+ - **Event folder attachments** — generic files uploaded directly to the event page (not under any contribution). Typical for short meetings like the LHC morning meeting, where a single slide deck is attached to the event itself.
77
+ - **Contribution attachments** — files attached to individual contributions/talks.
78
+
70
79
  ### Create Events
71
80
 
72
81
  ```bash
@@ -82,7 +91,25 @@ indico-cli create-event-with-agenda --title "Workshop" --category-id 4648 \
82
91
 
83
92
  ### All Commands
84
93
 
85
- Run `indico-cli --help` to see all available commands, or `indico-cli <command> --help` for command-specific options.
94
+ | Command | Description |
95
+ |---------|-------------|
96
+ | `configure` | Save Indico base URL and API token, and verify the connection. |
97
+ | `user-info` | Show the current user's name, email, and admin status. |
98
+ | `search-events` | List events in a category, with optional date range and public-only filter. |
99
+ | `search-by-term` | Full-text search across categories, events, contributions, attachments, and notes. |
100
+ | `search-categories` | Browse sub-categories of a given category. |
101
+ | `event-details` | Show event metadata, optionally with contributions and sessions. |
102
+ | `contributions` | List contributions (and optionally subcontributions) of an event. |
103
+ | `files` | List and download all files attached to an event — event-level materials, event-page folder attachments, and contribution attachments. |
104
+ | `download` | Download a single attachment by its URL. |
105
+ | `create-event` | Create a new event (lecture, meeting, or conference). |
106
+ | `create-session` | Create a session inside an event. |
107
+ | `create-contribution` | Create a contribution inside an event, optionally in a given session. |
108
+ | `add-timetable-entry` | Add a session block or contribution entry to the event timetable. |
109
+ | `delete-timetable-entry` | Remove an entry from the event timetable. |
110
+ | `create-event-with-agenda` | Create an event and populate its timetable from a JSON agenda file. |
111
+
112
+ Run `indico-cli --help` for the same list, or `indico-cli <command> --help` for command-specific options.
86
113
 
87
114
  ## Claude Code Skill
88
115
 
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.4.2'
22
+ __version_tuple__ = version_tuple = (0, 4, 2)
23
+
24
+ __commit_id__ = commit_id = None
@@ -128,11 +128,13 @@ def search_events(ctx, category_id, from_date, to_date, limit, only_public):
128
128
  @click.option("--type", "type_filter", default=None,
129
129
  type=click.Choice(["category", "event", "contribution", "subcontribution", "attachment", "event_note"]),
130
130
  help="Filter to a specific content type")
131
+ @click.option("--from-date", default=None, help="Start date filter (YYYY-MM-DD)")
132
+ @click.option("--to-date", default=None, help="End date filter (YYYY-MM-DD)")
131
133
  @click.pass_context
132
- def search_by_term(ctx, term, limit, type_filter):
134
+ def search_by_term(ctx, term, limit, type_filter, from_date, to_date):
133
135
  """Search for events by keyword/term."""
134
136
  client = get_client(ctx.obj["base_url"], ctx.obj["token"])
135
- result = run_async(client.search_events_by_term(term, limit, type_filter))
137
+ result = run_async(client.search_events_by_term(term, limit, type_filter, from_date, to_date))
136
138
  click.echo(result)
137
139
 
138
140
 
@@ -365,7 +365,8 @@ class IndicoClient:
365
365
  return response
366
366
 
367
367
  async def search_events_by_term(
368
- self, search_term: str, limit: int = 50, type_filter: Optional[str] = None
368
+ self, search_term: str, limit: int = 50, type_filter: Optional[str] = None,
369
+ from_date: Optional[str] = None, to_date: Optional[str] = None
369
370
  ) -> str:
370
371
  """Search for content by keyword/term using comprehensive Indico search API"""
371
372
  try:
@@ -387,6 +388,13 @@ class IndicoClient:
387
388
  else:
388
389
  search_types = all_search_types
389
390
 
391
+ # Build start_range filter if date params provided
392
+ start_range = None
393
+ if from_date or to_date:
394
+ range_start = from_date or "*"
395
+ range_end = to_date or "*"
396
+ start_range = f"[{range_start} TO {range_end}]"
397
+
390
398
  all_results = {}
391
399
  total_found = 0
392
400
 
@@ -396,6 +404,8 @@ class IndicoClient:
396
404
  params = {"q": search_term, "type": search_type}
397
405
  if limit:
398
406
  params["limit"] = str(limit)
407
+ if start_range:
408
+ params["start_range"] = start_range
399
409
 
400
410
  # Use the new search API endpoint
401
411
  result = await self.api.get(f"/search/api/search", params)
@@ -991,6 +1001,41 @@ class IndicoClient:
991
1001
  response += f" - {filename} ({file_size})\n"
992
1002
  response += "\n"
993
1003
 
1004
+ # 1b. Check event-level folders/attachments (generic attachments uploaded to the event page)
1005
+ event_folders = event.get("folders", [])
1006
+ if event_folders:
1007
+ event_folder_files_count = 0
1008
+ for folder in event_folders:
1009
+ folder_title = folder.get("title", "Unknown Folder")
1010
+ folder_id = folder.get("id", "")
1011
+ attachments = folder.get("attachments", [])
1012
+
1013
+ for attachment in attachments:
1014
+ filename = attachment.get(
1015
+ "filename",
1016
+ attachment.get("title", "unknown_file"),
1017
+ )
1018
+ attachment_id = attachment.get("id", "")
1019
+ file_size = attachment.get("size", "unknown size")
1020
+ download_url = attachment.get("download_url") or attachment.get("url")
1021
+
1022
+ files_found.append(
1023
+ {
1024
+ "filename": filename,
1025
+ "attachment_id": attachment_id,
1026
+ "folder_id": folder_id,
1027
+ "size": file_size,
1028
+ "source": f"Event Folder: {folder_title}",
1029
+ "type": "event_attachment",
1030
+ "download_url": download_url,
1031
+ "api_path": f"/event/{event_id}/attachments/{attachment_id}/{filename}",
1032
+ }
1033
+ )
1034
+ event_folder_files_count += 1
1035
+
1036
+ if event_folder_files_count > 0:
1037
+ response += f"Event Attachments: {event_folder_files_count} files found\n\n"
1038
+
994
1039
  # 2. Check contribution-level materials/attachments
995
1040
  contributions = event.get("contributions", [])
996
1041
  if contributions:
@@ -75,7 +75,7 @@ The goal is to answer the user's question, not just list metadata. If slides wer
75
75
  ### search-by-term — Full-text search across all Indico content
76
76
 
77
77
  ```bash
78
- uvx indico-cli search-by-term --term "<query>" [--limit N] [--type TYPE]
78
+ uvx indico-cli search-by-term --term "<query>" [--limit N] [--type TYPE] [--from-date YYYY-MM-DD] [--to-date YYYY-MM-DD]
79
79
  ```
80
80
 
81
81
  | Parameter | Required | Description |
@@ -83,8 +83,10 @@ uvx indico-cli search-by-term --term "<query>" [--limit N] [--type TYPE]
83
83
  | `--term` | Yes | Search keyword or phrase |
84
84
  | `--limit` | No | Max results per content type (default: 50) |
85
85
  | `--type` | No | Filter to one content type: `event`, `contribution`, `subcontribution`, `attachment`, `event_note`, `category` |
86
+ | `--from-date` | No | Start date filter (YYYY-MM-DD) |
87
+ | `--to-date` | No | End date filter (YYYY-MM-DD) |
86
88
 
87
- Searches categories, events, contributions, subcontributions, attachments, and notes.
89
+ Searches categories, events, contributions, subcontributions, attachments, and notes. Use `--from-date`/`--to-date` to narrow results to a specific time range.
88
90
 
89
91
  ### search-events — Search events in a category by date
90
92
 
@@ -159,6 +161,11 @@ uvx indico-cli files --event-id <ID> [--download | --no-download] [--download-di
159
161
  | `--download/--no-download` | No | Download files (default: download) |
160
162
  | `--download-dir` | No | Directory for downloads |
161
163
 
164
+ Covers three sources of files:
165
+ - Event-level legacy **materials** (`event.material[]`)
166
+ - Event-level **folder attachments** — generic files uploaded to the event page itself, not tied to any contribution (e.g. the single slide deck of an LHC morning meeting)
167
+ - **Contribution attachments** — files attached to individual talks/contributions
168
+
162
169
  If the built-in download fails (403), list with `--no-download` to get URLs, then use curl:
163
170
 
164
171
  ```bash
@@ -0,0 +1,61 @@
1
+ """Tests for get_files discovery of the different Indico attachment sources."""
2
+
3
+ import pytest
4
+ from unittest.mock import AsyncMock
5
+
6
+ from indico_cli.indico_api import IndicoAPI, IndicoClient
7
+
8
+
9
+ def make_client():
10
+ return IndicoClient(IndicoAPI("https://indico.cern.ch"))
11
+
12
+
13
+ EVENT_FOLDER_ATTACHMENT = {
14
+ "count": 1,
15
+ "results": [{
16
+ "title": "LHC morning meeting",
17
+ "folders": [{
18
+ "title": "Slides",
19
+ "id": 3254260,
20
+ "attachments": [{
21
+ "filename": "2026.04.13.morning-meeting.pdf",
22
+ "id": 5808485,
23
+ "size": 1073612,
24
+ "download_url": "https://indico.cern.ch/event/1649151/attachments/3254260/5808485/2026.04.13.morning-meeting.pdf",
25
+ }],
26
+ }],
27
+ "contributions": [],
28
+ }],
29
+ }
30
+
31
+
32
+ class TestEventFolderAttachments:
33
+ @pytest.mark.asyncio
34
+ async def test_lists_event_level_folder_attachment(self):
35
+ """Generic attachments uploaded to the event page (not a contribution) must be discovered."""
36
+ client = make_client()
37
+ client.api.get = AsyncMock(return_value=EVENT_FOLDER_ATTACHMENT)
38
+ result = await client.get_files("1649151", download_files=False, include_content=False)
39
+ assert "2026.04.13.morning-meeting.pdf" in result
40
+ assert "Event Attachments: 1 files found" in result
41
+ assert "Event Folder: Slides" in result
42
+ assert "Total files found: 1" in result
43
+
44
+ @pytest.mark.asyncio
45
+ async def test_event_folder_download_url_exposed(self):
46
+ """The attachment's download_url must be surfaced so downloads hit the correct path."""
47
+ client = make_client()
48
+ client.api.get = AsyncMock(return_value=EVENT_FOLDER_ATTACHMENT)
49
+ result = await client.get_files("1649151", download_files=False, include_content=False)
50
+ assert "/event/1649151/attachments/3254260/5808485/" in result
51
+
52
+ @pytest.mark.asyncio
53
+ async def test_no_folders_no_false_positive(self):
54
+ """Events without folder attachments must not report event attachments."""
55
+ client = make_client()
56
+ client.api.get = AsyncMock(return_value={
57
+ "count": 1,
58
+ "results": [{"title": "Empty", "folders": [], "contributions": []}],
59
+ })
60
+ result = await client.get_files("1", download_files=False, include_content=False)
61
+ assert "Event Attachments:" not in result
@@ -99,10 +99,84 @@ dependencies = [
99
99
  { name = "httpx" },
100
100
  ]
101
101
 
102
+ [package.optional-dependencies]
103
+ dev = [
104
+ { name = "pytest" },
105
+ { name = "pytest-asyncio" },
106
+ ]
107
+
102
108
  [package.metadata]
103
109
  requires-dist = [
104
110
  { name = "click", specifier = ">=8.0" },
105
111
  { name = "httpx", specifier = ">=0.25" },
112
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" },
113
+ { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21" },
114
+ ]
115
+ provides-extras = ["dev"]
116
+
117
+ [[package]]
118
+ name = "iniconfig"
119
+ version = "2.3.0"
120
+ source = { registry = "https://acc-py-repo.cern.ch/repository/vr-py-releases/simple" }
121
+ sdist = { url = "https://acc-py-repo.cern.ch/repository/vr-py-releases/resources/iniconfig/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43Z" }
122
+ wheels = [
123
+ { url = "https://acc-py-repo.cern.ch/repository/vr-py-releases/resources/iniconfig/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41Z" },
124
+ ]
125
+
126
+ [[package]]
127
+ name = "packaging"
128
+ version = "26.0"
129
+ source = { registry = "https://acc-py-repo.cern.ch/repository/vr-py-releases/simple" }
130
+ sdist = { url = "https://acc-py-repo.cern.ch/repository/vr-py-releases/resources/packaging/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39Z" }
131
+ wheels = [
132
+ { url = "https://acc-py-repo.cern.ch/repository/vr-py-releases/resources/packaging/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37Z" },
133
+ ]
134
+
135
+ [[package]]
136
+ name = "pluggy"
137
+ version = "1.6.0"
138
+ source = { registry = "https://acc-py-repo.cern.ch/repository/vr-py-releases/simple" }
139
+ sdist = { url = "https://acc-py-repo.cern.ch/repository/vr-py-releases/resources/pluggy/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07Z" }
140
+ wheels = [
141
+ { url = "https://acc-py-repo.cern.ch/repository/vr-py-releases/resources/pluggy/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06Z" },
142
+ ]
143
+
144
+ [[package]]
145
+ name = "pygments"
146
+ version = "2.20.0"
147
+ source = { registry = "https://acc-py-repo.cern.ch/repository/vr-py-releases/simple" }
148
+ sdist = { url = "https://acc-py-repo.cern.ch/repository/vr-py-releases/resources/pygments/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33Z" }
149
+ wheels = [
150
+ { url = "https://acc-py-repo.cern.ch/repository/vr-py-releases/resources/pygments/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30Z" },
151
+ ]
152
+
153
+ [[package]]
154
+ name = "pytest"
155
+ version = "9.0.3"
156
+ source = { registry = "https://acc-py-repo.cern.ch/repository/vr-py-releases/simple" }
157
+ dependencies = [
158
+ { name = "colorama", marker = "sys_platform == 'win32'" },
159
+ { name = "iniconfig" },
160
+ { name = "packaging" },
161
+ { name = "pluggy" },
162
+ { name = "pygments" },
163
+ ]
164
+ sdist = { url = "https://acc-py-repo.cern.ch/repository/vr-py-releases/resources/pytest/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18Z" }
165
+ wheels = [
166
+ { url = "https://acc-py-repo.cern.ch/repository/vr-py-releases/resources/pytest/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16Z" },
167
+ ]
168
+
169
+ [[package]]
170
+ name = "pytest-asyncio"
171
+ version = "1.3.0"
172
+ source = { registry = "https://acc-py-repo.cern.ch/repository/vr-py-releases/simple" }
173
+ dependencies = [
174
+ { name = "pytest" },
175
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
176
+ ]
177
+ sdist = { url = "https://acc-py-repo.cern.ch/repository/vr-py-releases/resources/pytest-asyncio/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47Z" }
178
+ wheels = [
179
+ { url = "https://acc-py-repo.cern.ch/repository/vr-py-releases/resources/pytest-asyncio/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45Z" },
106
180
  ]
107
181
 
108
182
  [[package]]
@@ -1,34 +0,0 @@
1
- # file generated by setuptools-scm
2
- # don't change, don't track in version control
3
-
4
- __all__ = [
5
- "__version__",
6
- "__version_tuple__",
7
- "version",
8
- "version_tuple",
9
- "__commit_id__",
10
- "commit_id",
11
- ]
12
-
13
- TYPE_CHECKING = False
14
- if TYPE_CHECKING:
15
- from typing import Tuple
16
- from typing import Union
17
-
18
- VERSION_TUPLE = Tuple[Union[int, str], ...]
19
- COMMIT_ID = Union[str, None]
20
- else:
21
- VERSION_TUPLE = object
22
- COMMIT_ID = object
23
-
24
- version: str
25
- __version__: str
26
- __version_tuple__: VERSION_TUPLE
27
- version_tuple: VERSION_TUPLE
28
- commit_id: COMMIT_ID
29
- __commit_id__: COMMIT_ID
30
-
31
- __version__ = version = '0.4.1'
32
- __version_tuple__ = version_tuple = (0, 4, 1)
33
-
34
- __commit_id__ = commit_id = None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes