Skip to content

Commit 675d0f9

Browse files
authored
Merge pull request #5 from RutgerRauws/feature/get-assembly
Add Assembly support and big refactoring using hooks instead of from_json functions
2 parents ce7ccd6 + 4e2a570 commit 675d0f9

23 files changed

+380
-147
lines changed

examples/get_assembly.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Add the notubiz folder to the path
2+
import sys, os
3+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
4+
5+
from notubiz import ApiClient, Configuration
6+
from notubiz.api.clients import AssemblyClient, MeetingClient
7+
8+
configuration = Configuration(organisation_id = 686) # Gemeente Eindhoven
9+
10+
api_client = ApiClient(configuration)
11+
12+
assembly_client = AssemblyClient(api_client)
13+
meeting_client = MeetingClient(api_client)
14+
15+
assembly = assembly_client.get(1253866)
16+
17+
for assembly_meeting in assembly.meetings:
18+
meeting = meeting_client.get(assembly_meeting.id)
19+
20+
print("{} - {} ({})".format(assembly_meeting.plannings[0].start_date, meeting.title, meeting.location))

examples/get_events.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313

1414
event_client = EventsClient(api_client)
1515

16-
start_date = datetime(2019, 1, 1)
17-
end_date = datetime(2020, 3, 31, 23, 59, 59)
16+
start_date = datetime(2025, 1, 5)
17+
end_date = datetime(2025, 1, 7, 23, 59, 59)
1818

1919
events = event_client.get(start_date, end_date)
2020

notubiz/api/_converter.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from cattr import Converter
2+
from datetime import datetime
3+
4+
from notubiz.api._helpers import parse_date
5+
from notubiz.api._hooks_registry import Hooks
6+
7+
def get_converter() -> Converter:
8+
converter = Converter()
9+
converter.register_structure_hook(datetime, lambda date_string, _: parse_date(date_string))
10+
11+
# Custom hooks
12+
for cls, hook in Hooks.items():
13+
converter.register_structure_hook(cls, hook)
14+
15+
return converter

notubiz/api/_hooks_registry.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from notubiz.api.dataclasses import Event, event_hook
2+
from notubiz.api.dataclasses import Meeting, meeting_hook
3+
from notubiz.api.dataclasses import AgendaItem, agenda_item_hook
4+
from notubiz.api.dataclasses import Speakers, Speaker, speakers_hook, speaker_hook
5+
6+
Hooks = {
7+
Event: event_hook,
8+
Meeting: meeting_hook,
9+
AgendaItem: agenda_item_hook,
10+
Speakers: speakers_hook,
11+
Speaker: speaker_hook
12+
}

notubiz/api/clients/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from notubiz.api.clients.events_client import EventsClient
22
from notubiz.api.clients.meeting_client import MeetingClient
3-
from notubiz.api.clients.speakers_client import SpeakersClient
3+
from notubiz.api.clients.speakers_client import SpeakersClient
4+
from notubiz.api.clients.assembly_client import AssemblyClient
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from notubiz import ApiClient
2+
from notubiz.api.dataclasses.assembly import Assembly
3+
4+
from notubiz.api._converter import get_converter
5+
6+
class AssemblyClient:
7+
api_client : ApiClient
8+
9+
def __init__(self, api_client : ApiClient):
10+
self.api_client = api_client
11+
12+
def get(self, assembly_event_id : int) -> Assembly:
13+
json_object = self.api_client.get("events/assemblies/{}".format(assembly_event_id))
14+
15+
json_assembly = json_object["assembly"]
16+
17+
return get_converter().structure(json_assembly, Assembly)

notubiz/api/clients/events_client.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from notubiz.api.dataclasses import Event
33
from datetime import datetime
44

5+
from notubiz.api._converter import get_converter
6+
57
class EventsClient:
68
api_client : ApiClient
79

@@ -10,6 +12,7 @@ def __init__(self, api_client : ApiClient):
1012

1113

1214
def get(self, date_from: datetime, date_to: datetime, gremia: list[int] = None) -> list[Event]:
15+
converter = get_converter()
1316

1417
json_events : list[dict] = []
1518
has_more_pages = True
@@ -34,4 +37,4 @@ def get(self, date_from: datetime, date_to: datetime, gremia: list[int] = None)
3437
page += 1
3538

3639
# Now run the deserialization based on the merged events
37-
return [Event.from_json(json_event) for json_event in json_events]
40+
return [converter.structure(json_event, Event) for json_event in json_events]

notubiz/api/clients/meeting_client.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from notubiz import ApiClient
22
from notubiz.api.dataclasses import Meeting
3+
from notubiz.api._converter import get_converter
34

45
class MeetingClient:
56
api_client : ApiClient
@@ -9,4 +10,5 @@ def __init__(self, api_client : ApiClient):
910

1011
def get(self, meeting_id : int) -> Meeting:
1112
json_object = self.api_client.get("events/meetings/{}".format(meeting_id))
12-
return Meeting.from_json(json_object)
13+
14+
return get_converter().structure(json_object, Meeting)

notubiz/api/clients/speakers_client.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from notubiz.api_client import ApiClient
22
from notubiz.api.dataclasses import Speakers
3+
from notubiz.api._converter import get_converter
34

45
class SpeakersClient:
56
api_client : ApiClient
@@ -9,4 +10,4 @@ def __init__(self, api_client : ApiClient):
910

1011
def get(self) -> Speakers:
1112
json_object = self.api_client.get("speakers")
12-
return Speakers.from_json(json_object)
13+
return get_converter().structure(json_object, Speakers)

notubiz/api/dataclasses/__init__.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
from notubiz.api.dataclasses.event import Event
2-
from notubiz.api.dataclasses.meeting import Meeting
1+
from notubiz.api.dataclasses.event import Event, event_hook
2+
from notubiz.api.dataclasses.meeting import Meeting, meeting_hook
3+
from notubiz.api.dataclasses.assembly import Assembly
34
from notubiz.api.dataclasses.document import Document
4-
from notubiz.api.dataclasses.agenda_item import AgendaItem
5-
from notubiz.api.dataclasses.speakers import Speaker, Speakers
5+
from notubiz.api.dataclasses.agenda_item import AgendaItem, agenda_item_hook
6+
from notubiz.api.dataclasses.speakers import Speakers, Speaker, speakers_hook, speaker_hook
+33-35
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,26 @@
11
from attrs import define, field
2-
import cattrs
2+
from cattr import Converter
33

44
from datetime import datetime
5-
from typing import Optional, Dict, Any
5+
from typing import Optional
66

77
from notubiz.api._helpers import parse_date, get_attribute, get_title, get_description
88
from notubiz.api.dataclasses.document import Document
99

1010
@define
1111
class AgendaItem:
12+
# Auto-filled fields
1213
id : int
1314
last_modified : datetime
14-
title : str
15-
description: str
16-
start_date : Optional[datetime]
17-
end_date : Optional[datetime]
18-
is_heading : bool
1915
documents: list[Document]
20-
agenda_items : list['AgendaItem'] = field(factory=list)
21-
22-
class AgendaItems:
23-
@staticmethod
24-
def from_json(json_object : any) -> list[AgendaItem]:
25-
c = cattrs.Converter()
26-
27-
c.register_structure_hook(datetime, lambda date_string, _: parse_date(date_string))
28-
c.register_structure_hook(AgendaItem, agenda_item_structure_hook)
29-
30-
agenda_items = [c.structure(item, AgendaItem) for item in json_object]
3116

32-
return agenda_items
33-
17+
# Filled manually
18+
title : str = field(init=False)
19+
description: str = field(init=False)
20+
start_date : Optional[datetime] = field(init=False)
21+
end_date : Optional[datetime] = field(init=False)
22+
is_heading : bool = field(init=False)
23+
agenda_items : list['AgendaItem'] = field(factory=list)
3424

3525
def get_start_date(attributes) -> datetime:
3626
try:
@@ -44,22 +34,30 @@ def get_end_date(attributes) -> datetime:
4434
except Exception: # The nested agenda items seem to have no end dates.
4535
return None
4636

47-
def agenda_item_structure_hook(data: Dict[str, Any], cls: type) -> AgendaItem:
48-
type_data = data.get("type_data", {})
49-
attributes = type_data["attributes"]
37+
def agenda_item_hook(data: dict[str, any], cls: type) -> AgendaItem:
38+
# Auto-fill fields
5039

51-
documents = [Document.from_json(item) for item in data["documents"]]
52-
agenda_items = AgendaItems.from_json(data["agenda_items"])
40+
converter = Converter()
41+
converter.register_structure_hook(datetime, lambda date_string, _: parse_date(date_string))
42+
converter.register_structure_hook(AgendaItem, agenda_item_hook)
43+
documents = [converter.structure(item, Document) for item in data.get("documents", [])]
5344

54-
return AgendaItem(
55-
id=data["id"],
56-
last_modified = parse_date(data["last_modified"]),
57-
title = get_title(attributes),
58-
description = get_description(attributes),
59-
start_date = get_start_date(attributes),
60-
end_date = get_end_date(attributes),
61-
is_heading = data["type_data"]["heading"],
62-
documents = documents,
63-
agenda_items = agenda_items
45+
agenda_item = AgendaItem(
46+
id = data.get("id"),
47+
last_modified = parse_date(data.get("last_modified")),
48+
documents = documents
6449
)
6550

51+
# Manually add some fields
52+
type_data = data.get("type_data", {})
53+
attributes = type_data["attributes"]
54+
55+
agenda_item.title = get_title(attributes)
56+
agenda_item.description = get_description(attributes)
57+
agenda_item.start_date = get_start_date(attributes)
58+
agenda_item.end_date = get_end_date(attributes)
59+
agenda_item.is_heading = type_data.get("heading", False)
60+
61+
agenda_item.agenda_items = [converter.structure(item, AgendaItem) for item in data.get("agenda_items", [])]
62+
63+
return agenda_item

notubiz/api/dataclasses/assembly.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from attrs import define
2+
3+
from notubiz.api.dataclasses.event import Event
4+
from notubiz.api.dataclasses.planning import Planning
5+
6+
@define
7+
class AssemblyMeeting:
8+
id: int
9+
order: int
10+
plannings: list[Planning]
11+
12+
@define
13+
class Assembly(Event):
14+
meetings: list[AssemblyMeeting]

notubiz/api/dataclasses/document.py

+6-12
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
from attrs import define, field
2-
import cattrs
32
from typing import Optional, List
43
from datetime import datetime
54

65
@define
76
class DocumentVersion:
87
id: int
9-
file_name: str
10-
file_size: int
11-
mime_type: Optional[str] = field(default="") # Apparently some documents have no MIME type
8+
type: str # Known possible values: file, link
9+
url: Optional[str] = field(default=None)
10+
file_name: Optional[str] = field(default=None)
11+
file_size: Optional[int] = field(default=None)
12+
mime_type: Optional[str] = field(default=None) # Some documents have no MIME type
1213

1314
@define
1415
class Document:
@@ -17,11 +18,4 @@ class Document:
1718
version: int
1819
url: str
1920
versions: List[DocumentVersion]
20-
id: Optional[int] = field(default=None) # Apparently some documents have no ID
21-
22-
@staticmethod
23-
def from_json(json_object : any) -> 'Document':
24-
c = cattrs.Converter()
25-
c.register_structure_hook(datetime, lambda d, _: datetime.strptime(d, "%Y-%m-%d %H:%M:%S"))
26-
27-
return c.structure(json_object, Document)
21+
id: Optional[int] = field(default=None) # Apparently some documents have no ID

notubiz/api/dataclasses/event.py

+14-25
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
11
from attrs import define, field
2-
import cattrs
3-
from cattrs import transform_error
4-
2+
from cattrs import Converter
53
from typing import Optional
64

75
from datetime import datetime
86
from notubiz.api._helpers import parse_date, get_title, get_location
9-
10-
@define
11-
class Planning:
12-
# Auto-filled
13-
start_date : datetime
14-
end_date : Optional[datetime]
7+
from notubiz.api.dataclasses.planning import Planning
158

169
@define
1710
class Event:
@@ -37,21 +30,17 @@ class Event:
3730
location: str = field(init=False)
3831
gremium_id: int = field(init=False)
3932

40-
@staticmethod
41-
def from_json(json_object : any) -> 'Event':
42-
c = cattrs.Converter()
43-
44-
c.register_structure_hook(datetime, lambda date_string, _: parse_date(date_string))
45-
46-
try:
47-
meeting = c.structure(json_object, Event)
48-
except Exception as exc:
49-
print("\n".join(transform_error(exc)))
50-
quit()
5133

52-
attributes = json_object.get("attributes", [])
53-
meeting.title = get_title(attributes)
54-
meeting.location = get_location(attributes)
55-
meeting.gremium_id = json_object["gremium"]["id"]
34+
def event_hook(data : dict, cls: type) -> Event:
35+
# Auto-fill fields
36+
converter = Converter()
37+
converter.register_structure_hook(datetime, lambda date_string, _: parse_date(date_string))
38+
event = converter.structure(data, cls)
5639

57-
return meeting
40+
# Manually add some fields
41+
attributes = data.get("attributes", [])
42+
event.title = get_title(attributes)
43+
event.location = get_location(attributes)
44+
event.gremium_id = data["gremium"]["id"]
45+
46+
return event

0 commit comments

Comments
 (0)