atlasdocs-indico 0.9.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.
- atlasdocs_indico-0.9.0/PKG-INFO +9 -0
- atlasdocs_indico-0.9.0/atlasdocs_indico/__init__.py +14 -0
- atlasdocs_indico-0.9.0/atlasdocs_indico/fetch_indico.py +231 -0
- atlasdocs_indico-0.9.0/atlasdocs_indico/indicoapi/__init__.py +23 -0
- atlasdocs_indico-0.9.0/atlasdocs_indico/indicoapi/category.py +26 -0
- atlasdocs_indico-0.9.0/atlasdocs_indico/indicoapi/contribution.py +20 -0
- atlasdocs_indico-0.9.0/atlasdocs_indico/indicoapi/event.py +30 -0
- atlasdocs_indico-0.9.0/atlasdocs_indico/indicoapi/indico.py +262 -0
- atlasdocs_indico-0.9.0/atlasdocs_indico/indicoapi/indicorequests.py +156 -0
- atlasdocs_indico-0.9.0/atlasdocs_indico/prepare_indico_nav.py +168 -0
- atlasdocs_indico-0.9.0/atlasdocs_indico/prepare_indico_pages.py +376 -0
- atlasdocs_indico-0.9.0/atlasdocs_indico/query_indico.py +159 -0
- atlasdocs_indico-0.9.0/pixi.toml +31 -0
- atlasdocs_indico-0.9.0/pyproject.toml +23 -0
- atlasdocs_indico-0.9.0/scripts/release.sh +79 -0
- atlasdocs_indico-0.9.0/scripts/test_indico_scripts.py +384 -0
- atlasdocs_indico-0.9.0/scripts/test_indicoapi.py +136 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: atlasdocs-indico
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: Indico integration scripts for atlasdocs-theme
|
|
5
|
+
Author-email: Jason Oliver <jason.oliver@cern.ch>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Requires-Dist: python-dotenv>=1.0
|
|
9
|
+
Requires-Dist: requests>=2.28
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def __getattr__(name: str):
|
|
5
|
+
if name == "create_indico_pages":
|
|
6
|
+
from .prepare_indico_pages import create_indico_pages
|
|
7
|
+
return create_indico_pages
|
|
8
|
+
if name == "create_indico_nav":
|
|
9
|
+
from .prepare_indico_nav import create_indico_nav
|
|
10
|
+
return create_indico_nav
|
|
11
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = ["create_indico_pages", "create_indico_nav"]
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Fetch Indico category events and save them as JSON files.
|
|
5
|
+
|
|
6
|
+
Reads which categories to fetch from indico.toml in the hub directory.
|
|
7
|
+
Writes one file per category:
|
|
8
|
+
data/indico/category/{id}/category_{id}.json
|
|
9
|
+
|
|
10
|
+
New events are merged with any existing file (most recent event wins on URL clash).
|
|
11
|
+
Requires an Indico API token via --token or the INDICO_API_TOKEN env variable.
|
|
12
|
+
|
|
13
|
+
Usage examples
|
|
14
|
+
--------------
|
|
15
|
+
Fetch all categories from indico.toml (no args needed):
|
|
16
|
+
python -m atlasdocs_indico.fetch_indico
|
|
17
|
+
|
|
18
|
+
Override categories from the command line:
|
|
19
|
+
python -m atlasdocs_indico.fetch_indico --category 1234
|
|
20
|
+
python -m atlasdocs_indico.fetch_indico --category 1234 --category 5678
|
|
21
|
+
|
|
22
|
+
Limit the date range:
|
|
23
|
+
python -m atlasdocs_indico.fetch_indico --years 2
|
|
24
|
+
python -m atlasdocs_indico.fetch_indico --start-date 2023-01-01
|
|
25
|
+
|
|
26
|
+
Pixi (args after --):
|
|
27
|
+
pixi run fetch-indico
|
|
28
|
+
pixi run fetch-indico -- --category 1234
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import argparse
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import sys
|
|
35
|
+
import tomllib
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
from dotenv import load_dotenv
|
|
39
|
+
load_dotenv(Path('.env').resolve())
|
|
40
|
+
|
|
41
|
+
# ── Configuration ──────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
INDICO_SITE = "https://indico.cern.ch"
|
|
44
|
+
DATA_DIR = "data/indico/category"
|
|
45
|
+
DEFAULT_YEARS = 2
|
|
46
|
+
INDICO_CONFIG_FILE = "indico.toml"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ── Config loading ─────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
def load_indico_config(hub_dir: str = ".") -> dict:
|
|
52
|
+
"""Load indico.toml. Returns {categories: [{id, label}], site, years}."""
|
|
53
|
+
config_path = Path(hub_dir) / INDICO_CONFIG_FILE
|
|
54
|
+
if not config_path.exists():
|
|
55
|
+
return {"categories": [], "site": INDICO_SITE, "years": DEFAULT_YEARS}
|
|
56
|
+
with open(config_path, "rb") as f:
|
|
57
|
+
data = tomllib.load(f)
|
|
58
|
+
return {
|
|
59
|
+
"categories": data.get("categories", []),
|
|
60
|
+
"site": data.get("site", INDICO_SITE),
|
|
61
|
+
"years": int(data.get("years", DEFAULT_YEARS)),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ── Fetch ──────────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
def fetch_category(
|
|
68
|
+
category_id: int,
|
|
69
|
+
api_token: str,
|
|
70
|
+
site: str = INDICO_SITE,
|
|
71
|
+
years: int = DEFAULT_YEARS,
|
|
72
|
+
start_date: str | None = None,
|
|
73
|
+
end_date: str | None = None,
|
|
74
|
+
) -> list[dict]:
|
|
75
|
+
from .indicoapi.indico import Indico
|
|
76
|
+
|
|
77
|
+
indico = Indico(
|
|
78
|
+
category_id=category_id,
|
|
79
|
+
api_token=api_token,
|
|
80
|
+
site=site,
|
|
81
|
+
years=years,
|
|
82
|
+
start_date=start_date,
|
|
83
|
+
end_date=end_date,
|
|
84
|
+
)
|
|
85
|
+
meetings = indico.get_meetings(include=None, exclude=None)
|
|
86
|
+
|
|
87
|
+
events = []
|
|
88
|
+
for event in meetings:
|
|
89
|
+
contributions = [
|
|
90
|
+
{
|
|
91
|
+
"title": c.title,
|
|
92
|
+
"url": c.url,
|
|
93
|
+
"speakers": c.speakers,
|
|
94
|
+
"minutes": c.minutes,
|
|
95
|
+
"start_time": c.start_time,
|
|
96
|
+
"attachments": c.attachments,
|
|
97
|
+
}
|
|
98
|
+
for c in event.contributions
|
|
99
|
+
]
|
|
100
|
+
events.append({
|
|
101
|
+
"url": event.url,
|
|
102
|
+
"date": event.date,
|
|
103
|
+
"title": event.title,
|
|
104
|
+
"minutes_url": event.minutes_url,
|
|
105
|
+
"contributions": contributions,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
return events
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ── Prepare ────────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
def save_category(category_id: int | str, events: list[dict], data_dir: str) -> Path:
|
|
114
|
+
cat_id = str(category_id)
|
|
115
|
+
out_path = Path(data_dir) / cat_id / f"category_{cat_id}.json"
|
|
116
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
|
|
118
|
+
existing: dict[str, dict] = {}
|
|
119
|
+
if out_path.exists():
|
|
120
|
+
try:
|
|
121
|
+
with open(out_path) as f:
|
|
122
|
+
for ev in json.load(f):
|
|
123
|
+
if ev.get("url"):
|
|
124
|
+
existing[ev["url"]] = ev
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
n_before = len(existing)
|
|
129
|
+
for ev in events:
|
|
130
|
+
if ev.get("url"):
|
|
131
|
+
existing[ev["url"]] = ev
|
|
132
|
+
|
|
133
|
+
merged = sorted(existing.values(), key=lambda e: e.get("date", ""), reverse=True)
|
|
134
|
+
with open(out_path, "w") as f:
|
|
135
|
+
json.dump(merged, f, separators=(",", ":"))
|
|
136
|
+
|
|
137
|
+
new_count = len(merged) - n_before
|
|
138
|
+
new_str = f"+{new_count} new" if new_count else "no change"
|
|
139
|
+
print(f"[indico] [{cat_id}] {len(merged)} events ({new_str}) → {out_path}")
|
|
140
|
+
return out_path
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def fetch_indico(
|
|
144
|
+
category_ids: list[int],
|
|
145
|
+
api_token: str,
|
|
146
|
+
site: str = INDICO_SITE,
|
|
147
|
+
years: int = DEFAULT_YEARS,
|
|
148
|
+
start_date: str | None = None,
|
|
149
|
+
end_date: str | None = None,
|
|
150
|
+
data_dir: str = DATA_DIR,
|
|
151
|
+
hub_dir: str = ".",
|
|
152
|
+
) -> None:
|
|
153
|
+
resolved_data_dir = str(Path(hub_dir) / data_dir)
|
|
154
|
+
errors = []
|
|
155
|
+
for cat_id in category_ids:
|
|
156
|
+
try:
|
|
157
|
+
events = fetch_category(
|
|
158
|
+
category_id=cat_id,
|
|
159
|
+
api_token=api_token,
|
|
160
|
+
site=site,
|
|
161
|
+
years=years,
|
|
162
|
+
start_date=start_date,
|
|
163
|
+
end_date=end_date,
|
|
164
|
+
)
|
|
165
|
+
save_category(cat_id, events, data_dir=resolved_data_dir)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
print(f"[indico] ERROR: category {cat_id}: {e}")
|
|
168
|
+
errors.append(cat_id)
|
|
169
|
+
|
|
170
|
+
if errors:
|
|
171
|
+
print(f"[indico] Done with {len(errors)} error(s): {errors}")
|
|
172
|
+
sys.exit(1)
|
|
173
|
+
n = len(category_ids)
|
|
174
|
+
print(f"[indico] Fetched {n} {'category' if n == 1 else 'categories'}")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def main() -> None:
|
|
178
|
+
parser = argparse.ArgumentParser(
|
|
179
|
+
description="Fetch Indico category events and save as JSON.",
|
|
180
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
181
|
+
epilog=__doc__.split("Usage examples")[1] if __doc__ and "Usage examples" in __doc__ else "",
|
|
182
|
+
)
|
|
183
|
+
parser.add_argument(
|
|
184
|
+
"--category", "-c",
|
|
185
|
+
metavar="ID",
|
|
186
|
+
action="append",
|
|
187
|
+
type=int,
|
|
188
|
+
help="Category ID to fetch (repeat for multiple: -c 1234 -c 5678). "
|
|
189
|
+
"Defaults to all categories listed in indico.toml.",
|
|
190
|
+
)
|
|
191
|
+
parser.add_argument("--token", help="Indico API token (default: $INDICO_API_TOKEN)")
|
|
192
|
+
parser.add_argument("--site", default=None, help=f"Indico base URL (default: from indico.toml or {INDICO_SITE})")
|
|
193
|
+
parser.add_argument("--years", type=int, default=None, help=f"Years of history to fetch (default: from indico.toml or {DEFAULT_YEARS})")
|
|
194
|
+
parser.add_argument("--start-date", metavar="YYYY-MM-DD", help="Override start date")
|
|
195
|
+
parser.add_argument("--end-date", metavar="YYYY-MM-DD", help="Override end date")
|
|
196
|
+
parser.add_argument("--data-dir", default=DATA_DIR, help=f"Output directory (default: {DATA_DIR})")
|
|
197
|
+
|
|
198
|
+
args = parser.parse_args()
|
|
199
|
+
|
|
200
|
+
hub_dir = str(Path.cwd())
|
|
201
|
+
config = load_indico_config(hub_dir)
|
|
202
|
+
|
|
203
|
+
api_token = args.token or os.environ.get("INDICO_API_TOKEN")
|
|
204
|
+
if not api_token:
|
|
205
|
+
parser.error("API token required — use --token or set INDICO_API_TOKEN")
|
|
206
|
+
|
|
207
|
+
# --category overrides config; fall back to all categories in indico.toml
|
|
208
|
+
if args.category:
|
|
209
|
+
category_ids = args.category
|
|
210
|
+
else:
|
|
211
|
+
category_ids = [int(c["id"]) for c in config["categories"]]
|
|
212
|
+
if not category_ids:
|
|
213
|
+
parser.error("No categories specified — pass --category or add entries to indico.toml")
|
|
214
|
+
|
|
215
|
+
site = args.site or config["site"]
|
|
216
|
+
years = args.years or config["years"]
|
|
217
|
+
|
|
218
|
+
fetch_indico(
|
|
219
|
+
category_ids=category_ids,
|
|
220
|
+
api_token=api_token,
|
|
221
|
+
site=site,
|
|
222
|
+
years=years,
|
|
223
|
+
start_date=args.start_date,
|
|
224
|
+
end_date=args.end_date,
|
|
225
|
+
data_dir=args.data_dir,
|
|
226
|
+
hub_dir=hub_dir,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
if __name__ == "__main__":
|
|
231
|
+
main()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def __getattr__(name: str):
|
|
5
|
+
if name == "Category":
|
|
6
|
+
from .category import Category
|
|
7
|
+
return Category
|
|
8
|
+
if name == "Event":
|
|
9
|
+
from .event import Event
|
|
10
|
+
return Event
|
|
11
|
+
if name == "Contribution":
|
|
12
|
+
from .contribution import Contribution
|
|
13
|
+
return Contribution
|
|
14
|
+
if name == "Indico":
|
|
15
|
+
from .indico import Indico
|
|
16
|
+
return Indico
|
|
17
|
+
if name == "BearerAuth":
|
|
18
|
+
from .indicorequests import BearerAuth
|
|
19
|
+
return BearerAuth
|
|
20
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = ["Category", "Event", "Contribution", "Indico", "BearerAuth"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Category data model for Indico API."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
# ── Configuration ──────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
INDICO_BASE_URL = "https://indico.cern.ch"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Category:
|
|
12
|
+
"""Represents an Indico category."""
|
|
13
|
+
|
|
14
|
+
category_id: str
|
|
15
|
+
name: str
|
|
16
|
+
num_events: int
|
|
17
|
+
is_protected: bool = False
|
|
18
|
+
parent_id: str = None
|
|
19
|
+
url: str = None
|
|
20
|
+
immediate_children: list = None
|
|
21
|
+
|
|
22
|
+
def __post_init__(self):
|
|
23
|
+
if self.immediate_children is None:
|
|
24
|
+
self.immediate_children = []
|
|
25
|
+
if self.url is None:
|
|
26
|
+
self.url = f"{INDICO_BASE_URL}/category/{self.category_id}/"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Contribution data model for Indico API - pure data, no rendering."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Contribution:
|
|
8
|
+
"""Represents an Indico contribution with pure data (no markdown rendering)."""
|
|
9
|
+
|
|
10
|
+
title: str
|
|
11
|
+
url: str
|
|
12
|
+
speakers: list[str] = field(default_factory=list)
|
|
13
|
+
minutes: dict = None
|
|
14
|
+
start_time: str = None
|
|
15
|
+
end_time: str = None
|
|
16
|
+
duration: int = None
|
|
17
|
+
attachments: list[dict] = field(default_factory=list)
|
|
18
|
+
description: str = None
|
|
19
|
+
keywords: list[str] = field(default_factory=list)
|
|
20
|
+
contribution_type: str = None
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Event data model for Indico API - pure data, no rendering."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Event:
|
|
8
|
+
"""Represents an Indico event with pure data (no markdown rendering)."""
|
|
9
|
+
|
|
10
|
+
url: str
|
|
11
|
+
date: str
|
|
12
|
+
title: str
|
|
13
|
+
contributions: list[dict]
|
|
14
|
+
time: str = None
|
|
15
|
+
end_date: str = None
|
|
16
|
+
end_time: str = None
|
|
17
|
+
room: str = None
|
|
18
|
+
location: str = None
|
|
19
|
+
minutes_url: str = None
|
|
20
|
+
created_dt: str = None
|
|
21
|
+
event_type: str = None
|
|
22
|
+
description: str = None
|
|
23
|
+
chairpersons: list[dict] = field(default_factory=list)
|
|
24
|
+
label: dict = None
|
|
25
|
+
category: str = None
|
|
26
|
+
category_id: int = None
|
|
27
|
+
|
|
28
|
+
def __lt__(self, other):
|
|
29
|
+
"""Sort by date (newest first)."""
|
|
30
|
+
return self.date > other.date
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import functools
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
from . import indicorequests as ireq
|
|
9
|
+
from .contribution import Contribution
|
|
10
|
+
from .event import Event
|
|
11
|
+
|
|
12
|
+
REQUEST_DELAY = 0 # seconds between pagination calls
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _paginate(fetch_fn, page_size: int, log_prefix: str) -> tuple[list, int]:
|
|
16
|
+
"""Returns (results, number_of_api_calls).
|
|
17
|
+
|
|
18
|
+
Stops when Indico returns an empty page. With detail=contributions the limit
|
|
19
|
+
controls a contribution offset (not a hard cap on returned events), so pages
|
|
20
|
+
routinely come back with fewer than page_size events even when more exist.
|
|
21
|
+
"""
|
|
22
|
+
all_results, offset, page_num = [], 0, 1
|
|
23
|
+
while True:
|
|
24
|
+
page = fetch_fn(offset=offset)
|
|
25
|
+
all_results.extend(page)
|
|
26
|
+
if not page:
|
|
27
|
+
break
|
|
28
|
+
offset += page_size
|
|
29
|
+
page_num += 1
|
|
30
|
+
if REQUEST_DELAY > 0:
|
|
31
|
+
time.sleep(REQUEST_DELAY)
|
|
32
|
+
return all_results, page_num
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Indico:
|
|
37
|
+
# Accepts a single int or a list of ints; always stored as category_ids list.
|
|
38
|
+
category_id: int | list[int]
|
|
39
|
+
api_token: str = None
|
|
40
|
+
site: str = "https://indico.cern.ch"
|
|
41
|
+
years: int = 1
|
|
42
|
+
# Explicit date overrides — take priority over years when provided (YYYY-MM-DD).
|
|
43
|
+
start_date: str | None = None
|
|
44
|
+
end_date: str | None = None
|
|
45
|
+
|
|
46
|
+
def __post_init__(self):
|
|
47
|
+
if self.api_token is None:
|
|
48
|
+
self.api_token = os.environ.get("INDICO_API_TOKEN", None)
|
|
49
|
+
if self.api_token is None:
|
|
50
|
+
msg = "Please provide an API token or set INDICO_API_TOKEN in your env"
|
|
51
|
+
raise ValueError(msg)
|
|
52
|
+
|
|
53
|
+
# Normalise to list regardless of whether a single int was supplied.
|
|
54
|
+
if isinstance(self.category_id, int):
|
|
55
|
+
self.category_ids: list[int] = [self.category_id]
|
|
56
|
+
else:
|
|
57
|
+
self.category_ids = list(self.category_id)
|
|
58
|
+
|
|
59
|
+
self.req_kwargs = {"site": self.site, "api_token": self.api_token}
|
|
60
|
+
|
|
61
|
+
today = datetime.datetime.now(tz=datetime.timezone.utc).date()
|
|
62
|
+
if self.start_date is None:
|
|
63
|
+
self.start_date = (
|
|
64
|
+
today - datetime.timedelta(days=365 * self.years)
|
|
65
|
+
).strftime("%Y-%m-%d")
|
|
66
|
+
if self.end_date is None:
|
|
67
|
+
self.end_date = (today + datetime.timedelta(days=15)).strftime("%Y-%m-%d")
|
|
68
|
+
|
|
69
|
+
# Legacy single-category URL points at the primary (first) category.
|
|
70
|
+
self.url = f"{self.site}/category/{self.category_ids[0]}/"
|
|
71
|
+
|
|
72
|
+
all_events: list = []
|
|
73
|
+
self.api_call_count: int = 0
|
|
74
|
+
|
|
75
|
+
for cat_id in self.category_ids:
|
|
76
|
+
fetch_fn = functools.partial(
|
|
77
|
+
ireq.request_category_contributions,
|
|
78
|
+
category_id=cat_id,
|
|
79
|
+
start_date=self.start_date,
|
|
80
|
+
end_date=self.end_date,
|
|
81
|
+
limit=ireq.CONTRIBUTIONS_PAGE_SIZE,
|
|
82
|
+
**self.req_kwargs,
|
|
83
|
+
)
|
|
84
|
+
events, calls = _paginate(
|
|
85
|
+
fetch_fn=fetch_fn,
|
|
86
|
+
page_size=ireq.CONTRIBUTIONS_PAGE_SIZE,
|
|
87
|
+
log_prefix=str(cat_id),
|
|
88
|
+
)
|
|
89
|
+
all_events.extend(events)
|
|
90
|
+
self.api_call_count += calls
|
|
91
|
+
|
|
92
|
+
if len(self.category_ids) > 1 and REQUEST_DELAY > 0:
|
|
93
|
+
time.sleep(REQUEST_DELAY)
|
|
94
|
+
|
|
95
|
+
self.events = all_events
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def filter_events(
|
|
99
|
+
results: list,
|
|
100
|
+
include: list[str] | None = None,
|
|
101
|
+
exclude: list[str] | None = None,
|
|
102
|
+
) -> list:
|
|
103
|
+
if include is None:
|
|
104
|
+
include = []
|
|
105
|
+
if exclude is None:
|
|
106
|
+
exclude = []
|
|
107
|
+
include = [x.lower() for x in include]
|
|
108
|
+
exclude = [x.lower() for x in exclude]
|
|
109
|
+
|
|
110
|
+
if not include:
|
|
111
|
+
return results
|
|
112
|
+
|
|
113
|
+
return [
|
|
114
|
+
r
|
|
115
|
+
for r in results
|
|
116
|
+
if any(x in r["title"].lower() for x in include)
|
|
117
|
+
and not any(x in r["title"].lower() for x in exclude)
|
|
118
|
+
and not (
|
|
119
|
+
"label" in r
|
|
120
|
+
and r["label"] is not None
|
|
121
|
+
and r["label"].get("title").lower() == "cancelled"
|
|
122
|
+
)
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
def get_meetings(
|
|
126
|
+
self,
|
|
127
|
+
include: list[str] | None = None,
|
|
128
|
+
exclude: list[str] | None = None,
|
|
129
|
+
events_override: list | None = None,
|
|
130
|
+
) -> list[Event]:
|
|
131
|
+
"""Return filtered meetings as typed Event objects with Contribution objects."""
|
|
132
|
+
if exclude is None:
|
|
133
|
+
exclude = []
|
|
134
|
+
exclude += ["cancelled"]
|
|
135
|
+
raw_events = events_override if events_override is not None else self.events
|
|
136
|
+
filtered = self.filter_events(raw_events, include=include, exclude=exclude)
|
|
137
|
+
|
|
138
|
+
results = []
|
|
139
|
+
for event in filtered:
|
|
140
|
+
contributions = []
|
|
141
|
+
for c in event.get("contributions", []):
|
|
142
|
+
attachments = []
|
|
143
|
+
for folder in c.get("folders", []):
|
|
144
|
+
for a in folder.get("attachments", []):
|
|
145
|
+
attachments.append(
|
|
146
|
+
{
|
|
147
|
+
"url": a.get("download_url", ""),
|
|
148
|
+
"filename": a.get(
|
|
149
|
+
"filename", a.get("title", "unknown")
|
|
150
|
+
),
|
|
151
|
+
"size": a.get("size"),
|
|
152
|
+
"content_type": a.get("content_type"),
|
|
153
|
+
"title": a.get("title"),
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
contributions.append(
|
|
157
|
+
Contribution(
|
|
158
|
+
title=c.get("title", ""),
|
|
159
|
+
url=c.get("url", ""),
|
|
160
|
+
speakers=c.get("speakers", []),
|
|
161
|
+
minutes=c.get("note"),
|
|
162
|
+
start_time=(c.get("startDate") or {}).get("time"),
|
|
163
|
+
end_time=(c.get("endDate") or {}).get("time"),
|
|
164
|
+
duration=c.get("duration"),
|
|
165
|
+
attachments=attachments,
|
|
166
|
+
description=c.get("description"),
|
|
167
|
+
keywords=c.get("keywords") or [],
|
|
168
|
+
contribution_type=(c.get("type") or {}).get("name") if c.get("type") else None,
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
results.append(
|
|
172
|
+
Event(
|
|
173
|
+
url=event.get("url", ""),
|
|
174
|
+
date=(event.get("startDate") or {}).get("date", ""),
|
|
175
|
+
title=event.get("title", ""),
|
|
176
|
+
contributions=contributions,
|
|
177
|
+
location=event.get("location"),
|
|
178
|
+
room=event.get("room"),
|
|
179
|
+
time=(event.get("startDate") or {}).get("time"),
|
|
180
|
+
end_date=(event.get("endDate") or {}).get("date"),
|
|
181
|
+
end_time=(event.get("endDate") or {}).get("time"),
|
|
182
|
+
minutes_url=(
|
|
183
|
+
event["note"]["url"]
|
|
184
|
+
if event.get("note") and "url" in event["note"]
|
|
185
|
+
else None
|
|
186
|
+
),
|
|
187
|
+
created_dt=event.get("createdDT"),
|
|
188
|
+
event_type=event.get("type"),
|
|
189
|
+
description=event.get("description"),
|
|
190
|
+
chairpersons=event.get("chairpersons") or [],
|
|
191
|
+
label=event.get("label"),
|
|
192
|
+
category=event.get("category"),
|
|
193
|
+
category_id=event.get("categoryId"),
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return sorted(results, key=lambda e: e.date, reverse=True)
|
|
198
|
+
|
|
199
|
+
def get_meetings_for_author(self, author: str) -> list[Event]:
|
|
200
|
+
"""Return meetings where all tokens of `author` appear in a speaker's full_name."""
|
|
201
|
+
tokens = author.lower().split()
|
|
202
|
+
matching = []
|
|
203
|
+
for event in self.events:
|
|
204
|
+
for contrib in event.get("contributions", []):
|
|
205
|
+
if any(
|
|
206
|
+
all(
|
|
207
|
+
t in (s.get("fullName") or s.get("full_name") or "").lower()
|
|
208
|
+
for t in tokens
|
|
209
|
+
)
|
|
210
|
+
for s in contrib.get("speakers", [])
|
|
211
|
+
):
|
|
212
|
+
matching.append(event)
|
|
213
|
+
break
|
|
214
|
+
return self.get_meetings(events_override=matching)
|
|
215
|
+
|
|
216
|
+
def get_ical(
|
|
217
|
+
self,
|
|
218
|
+
extra_category_ids: list[int] | None = None,
|
|
219
|
+
start_date: str | None = None,
|
|
220
|
+
end_date: str | None = None,
|
|
221
|
+
) -> str:
|
|
222
|
+
"""Return a combined iCal string for all stored categories (+ any extras).
|
|
223
|
+
|
|
224
|
+
Paginates automatically if the range contains > 1000 events.
|
|
225
|
+
Defaults to 60 days in the past through 15 days in the future.
|
|
226
|
+
"""
|
|
227
|
+
today = datetime.datetime.now(tz=datetime.timezone.utc).date()
|
|
228
|
+
if start_date is None:
|
|
229
|
+
start_date = (today - datetime.timedelta(days=60)).strftime("%Y-%m-%d")
|
|
230
|
+
if end_date is None:
|
|
231
|
+
end_date = (today + datetime.timedelta(days=15)).strftime("%Y-%m-%d")
|
|
232
|
+
ids = self.category_ids + (extra_category_ids or [])
|
|
233
|
+
pages = []
|
|
234
|
+
offset = 0
|
|
235
|
+
page_num = 1
|
|
236
|
+
while True:
|
|
237
|
+
page = ireq.request_category_ical(
|
|
238
|
+
category_ids=ids,
|
|
239
|
+
start_date=start_date,
|
|
240
|
+
end_date=end_date,
|
|
241
|
+
offset=offset,
|
|
242
|
+
**self.req_kwargs,
|
|
243
|
+
)
|
|
244
|
+
n_events = page.count("BEGIN:VEVENT")
|
|
245
|
+
pages.append(page)
|
|
246
|
+
if n_events < ireq.ICAL_PAGE_SIZE:
|
|
247
|
+
break
|
|
248
|
+
offset += ireq.ICAL_PAGE_SIZE
|
|
249
|
+
page_num += 1
|
|
250
|
+
if REQUEST_DELAY > 0:
|
|
251
|
+
time.sleep(REQUEST_DELAY)
|
|
252
|
+
|
|
253
|
+
return _merge_ical_pages(pages)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _merge_ical_pages(pages: list[str]) -> str:
|
|
257
|
+
if not pages:
|
|
258
|
+
return ""
|
|
259
|
+
all_text = "\n".join(pages)
|
|
260
|
+
events = re.findall(r"BEGIN:VEVENT.*?END:VEVENT", all_text, re.DOTALL)
|
|
261
|
+
header = pages[0].split("BEGIN:VEVENT")[0].rstrip()
|
|
262
|
+
return "\n".join([header] + events + ["END:VCALENDAR"])
|