granicus_archiver.clips.model¶
- granicus_archiver.clips.model.ClipFileKey¶
Key to for file types in
ParseClipLinksandClipFilesalias of
Literal[‘agenda’, ‘minutes’, ‘audio’, ‘video’]
- granicus_archiver.clips.model.ClipFileUploadKey¶
Key for file types in
ClipFilesalias of
Literal[‘agenda’, ‘minutes’, ‘audio’, ‘video’] |Literal[‘chapters’, ‘agenda_packet’]
- class granicus_archiver.clips.model.ParseClipLinks(agenda: URL | None = None, minutes: URL | None = None, audio: URL | None = None, video: URL | None = None)[source]¶
Bases:
SerializableLinks for clip assets
- merge(other: ParseClipLinks) bool[source]¶
Merge any data missing in self from other
- Parameters:
other (ParseClipLinks)
- Return type:
- class granicus_archiver.clips.model.ParseClipData(id: CLIP_ID, location: Location, name: str, date: int, duration: int, original_links: ParseClipLinks, actual_links: ParseClipLinks | None = None, player_link: URL | None = None)[source]¶
Bases:
SerializableData for a clip parsed from granicus
- Parameters:
id (CLIP_ID)
location (Location)
name (str)
date (int)
duration (int)
original_links (ParseClipLinks)
actual_links (ParseClipLinks | None)
player_link (URL | None)
- original_links: ParseClipLinks¶
The asset links as reported by granicus. Some will be actually be redirects to a PDF viewer which will need to be resolved
- actual_links: ParseClipLinks | None = None¶
The
original_linksafter the redirects have been resolved
- iter_incomplete_links() Iterator[tuple[Literal['agenda', 'minutes', 'audio', 'video'], URL]][source]¶
Iterate over links existing in
original_linksbut missing fromactual_links
- has_incomplete_links()[source]¶
Check if any links in
original_linksare missing fromactual_links
- build_fs_dir(root_dir: Path | None, replace_invalid: bool = True) Path[source]¶
Create a path for the clip within the given root_dir
If replace_invalid is True, forward slashes (“/”) will be replaced with colons (“:”) to prevent invalid and unexpected path names
- Parameters:
root_dir (Path | None)
replace_invalid (bool)
- Return type:
Path
- exception granicus_archiver.clips.model.CheckError(clip: Clip, key: Literal['agenda', 'minutes', 'audio', 'video', 'chapters', 'agenda_packet'], msg: str | None = None)[source]¶
Bases:
ExceptionBase exception for
ClipFiles.check()
- exception granicus_archiver.clips.model.NoMetaError(clip: Clip, key: Literal['agenda', 'minutes', 'audio', 'video', 'chapters', 'agenda_packet'], msg: str | None = None)[source]¶
Bases:
CheckErrorRaised when a file exists locally with no stored
FileMeta
- exception granicus_archiver.clips.model.ContentLengthError(clip: Clip, key: Literal['agenda', 'minutes', 'audio', 'video', 'chapters', 'agenda_packet'], msg: str | None = None)[source]¶
Bases:
CheckErrorRaised when the stored
content_lengthdoes not match the local filesize
- exception granicus_archiver.clips.model.ContentTypeError(clip: Clip, key: Literal['agenda', 'minutes', 'audio', 'video', 'chapters', 'agenda_packet'], msg: str | None = None)[source]¶
Bases:
CheckErrorRaised when the
content_typedisagrees with the file extension- Parameters:
- Return type:
None
- exception granicus_archiver.clips.model.NoFileError(clip: Clip, key: Literal['agenda', 'minutes', 'audio', 'video', 'chapters', 'agenda_packet'], msg: str | None = None)[source]¶
Bases:
CheckErrorRaised when there is
metadatafor a file that does not exist locally
- exception granicus_archiver.clips.model.FileShouldNotExist(clip: Clip, key: Literal['agenda', 'minutes', 'audio', 'video', 'chapters', 'agenda_packet'], msg: str | None = None)[source]¶
Bases:
CheckErrorRaised when the stored metadata for a file is
zero-lengthbut the file exists locally
- exception granicus_archiver.clips.model.FilesizeMagicNumber(clip: Clip, key: Literal['agenda', 'minutes', 'audio', 'video', 'chapters', 'agenda_packet'], msg: str | None = None)[source]¶
Bases:
CheckErrorRaised when the filesize is exactly
1245bytesDon’t ask me why, but this happens with some pdf downloads. It usually means the file can be re-downloaded because the Content-Length in the header shows a different value.
(I stopped asking why their systems are the way they are a long time ago)
- Parameters:
- Return type:
None
- classmethod is_magic_number(item: Path | FileMeta | int) bool[source]¶
Check if the value equals
1245This seriously feels a bit like the is-thirteen package.
- class granicus_archiver.clips.model.ClipFiles(clip: ~granicus_archiver.clips.model.Clip, agenda: ~pathlib._local.Path | None, minutes: ~pathlib._local.Path | None, audio: ~pathlib._local.Path | None, video: ~pathlib._local.Path | None, chapters: ~pathlib._local.Path | None = None, agenda_packet: ~pathlib._local.Path | None = None, metadata: dict[~typing.Literal['agenda', 'minutes', 'audio', 'video', 'chapters', 'agenda_packet'], ~granicus_archiver.types.FileMeta] = <factory>)[source]¶
Bases:
SerializableFile information for a
Clip- Parameters:
- chapters: Path | None = None¶
WebVTT chapters filename (built by
AgendaTimestamps.build_vtt())
- agenda_packet: Path | None = None¶
Agenda packet (parsed from
legistar.detail_page.DetailPageResult)
- metadata: dict[Literal['agenda', 'minutes', 'audio', 'video', 'chapters', 'agenda_packet'], FileMeta]¶
FileMetafor each file (if available)
- classmethod from_parse_data(clip: Clip, parse_data: ParseClipData) Self[source]¶
Create an instance from a
ParseClipDatainstance- Parameters:
clip (Clip)
parse_data (ParseClipData)
- Return type:
- classmethod build_path(root_dir: Path, key: Literal['agenda', 'minutes', 'audio', 'video', 'chapters', 'agenda_packet']) Path[source]¶
Build the filename for the given file type with root_dir prepended
- Parameters:
root_dir (Path)
key (Literal['agenda', 'minutes', 'audio', 'video', 'chapters', 'agenda_packet'])
- Return type:
Path
- ensure_path(key: Literal['agenda', 'minutes', 'audio', 'video', 'chapters', 'agenda_packet']) None[source]¶
Ensure path for key is set on the instance
- Raises:
ValueError – If the file does not exist
- Parameters:
key (Literal['agenda', 'minutes', 'audio', 'video', 'chapters', 'agenda_packet'])
- Return type:
None
- get_metadata(key: Literal['agenda', 'minutes', 'audio', 'video', 'chapters', 'agenda_packet']) FileMeta | None[source]¶
Get the
FileMetafor the given file type (if available)
- set_metadata(key: Literal['agenda', 'minutes', 'audio', 'video', 'chapters', 'agenda_packet'], meta: FileMeta | MultiMapping[str] | dict[str, str]) FileMeta[source]¶
Set the
FileMetafor the given file type from request headers
- check_chapters_file() bool[source]¶
Check for an existing
chaptersfileIf
chaptersis not set and the expected filename for it exists, the filename and itsmetadatawill be added.- Return type:
- check()[source]¶
Check local files against the stored
metadata- Raises:
CheckError – A subclass of
CheckErrorif any errors are found
- ensure_local_hashes(check_existing: bool = False) bool[source]¶
Ensure that all local files have an
sha1hash stored inmetadata
- iter_existing(for_download: bool = True) Iterator[tuple[Literal['agenda', 'minutes', 'audio', 'video'], Path]][source]¶
- iter_existing(for_download: bool = False) Iterator[tuple[Literal['agenda', 'minutes', 'audio', 'video', 'chapters', 'agenda_packet'], Path]]
Iterate over existing filenames
- Parameters:
for_download – If
True(the default), only the filenames expected to be on the Granicus server will be yielded. IfFalse, locally-generated files will be included (such aschapters).- Yields:
- key: The file key as
ClipFileKey(orClipFileUploadKey if for_download it True)
- key: The file key as
filename: The relative
Pathfor the file
- class granicus_archiver.clips.model.AgendaTimestamp(seconds: int, text: str)[source]¶
Bases:
SerializableA timestamped agenda item
- class granicus_archiver.clips.model.AgendaTimestamps(clip_id: CLIP_ID, items: list[AgendaTimestamp])[source]¶
Bases:
SerializableCollection of
AgendaTimestampfor aClip- Parameters:
clip_id (CLIP_ID)
items (list[AgendaTimestamp])
- items: list[AgendaTimestamp]¶
Timestamps for the clip
- class granicus_archiver.clips.model.AgendaTimestampCollection(clips: dict[~granicus_archiver.clips.model.CLIP_ID, ~granicus_archiver.clips.model.AgendaTimestamps] = <factory>)[source]¶
Bases:
SerializableContainer for
AgendaTimestamps- Parameters:
clips (dict[CLIP_ID, AgendaTimestamps])
- clips: dict[CLIP_ID, AgendaTimestamps]¶
- save(filename: PathLike, indent: int | None = 2) None[source]¶
Saves all data as JSON to the given filename
- add(item: AgendaTimestamps) None[source]¶
Add an
AgendaTimestampsinstance- Parameters:
item (AgendaTimestamps)
- Return type:
None
- get(key: CLIP_ID | Clip) AgendaTimestamps | None[source]¶
Get an
AgendaTimestampsobject if it existsThe key can be a
Clipinstance or the clip’sid- Parameters:
- Return type:
AgendaTimestamps | None
- class granicus_archiver.clips.model.Clip(parse_data: ParseClipData, root_dir: Path, parent: ClipCollection)[source]¶
Bases:
SerializableStores all information for a single clip
- Parameters:
parse_data (ParseClipData)
root_dir (Path)
parent (ClipCollection)
- parent: ClipCollection¶
The parent
ClipCollection
- property id: CLIP_ID¶
Alias for
ParseClipData.id
- property name: str¶
Alias for
ParseClipData.name
- property unique_name: str¶
Alias for
ParseClipData.unique_name
- property location: Location¶
Alias for
ParseClipData.location
- get_file_path(key: Literal['agenda', 'minutes', 'audio', 'video', 'chapters', 'agenda_packet'], absolute: bool = False) Path[source]¶
Get the relative or absolute path for the given file type
- classmethod from_parse_data(parent: ClipCollection, parse_data: ParseClipData) Self[source]¶
Create an instance from a
ParseClipDatainstance- Parameters:
parent (ClipCollection)
parse_data (ParseClipData)
- Return type:
- iter_url_paths(actual: bool = True, absolute: bool = True) Iterator[tuple[Literal['agenda', 'minutes', 'audio', 'video'], URL, Path]][source]¶
Iterate over the clip’s file types, url’s and filenames
- iter_paths(absolute: bool = True, for_download: bool = False) Iterator[tuple[Literal['agenda', 'minutes', 'audio', 'video', 'chapters', 'agenda_packet'], Path]][source]¶
- iter_paths(absolute: bool = True, for_download: bool = True) Iterator[tuple[Literal['agenda', 'minutes', 'audio', 'video'], Path]]
Iterate over paths in
files- Parameters:
absolute – Whether to yield absolute paths
for_download – If
True(the default), only the filenames expected to be on the Granicus server will be yielded. IfFalse, locally-generated files will be included (such asClipFiles.chapters).
- Yields:
- key: The file key as
ClipFileKey(orClipFileUploadKey if for_download it True)
- key: The file key as
- filename: The
Pathfor the file relative to the root_dir(orroot_dir_absif absolute is True).
- filename: The
- class granicus_archiver.clips.model.ClipCollection(base_dir: ~pathlib._local.Path, clips: dict[~granicus_archiver.clips.model.CLIP_ID, ~granicus_archiver.clips.model.Clip] = <factory>)[source]¶
Bases:
SerializableContainer for
Clips- base_dir: Path¶
Root filesystem path for the clip assets
- save(filename: PathLike, indent: int | None = 2) None[source]¶
Saves all clip data as JSON to the given filename
- add_clip(parse_data: ParseClipData) Clip[source]¶
Parse a
Clipfrom theParseClipDataand add it to the collection- Parameters:
parse_data (ParseClipData)
- Return type:
- merge(other: ClipCollection) ClipCollection[source]¶
Merge the clips in this instance with another
- Parameters:
other (ClipCollection)
- Return type:
- class granicus_archiver.clips.model.ClipIndex(id: CLIP_ID, location: Location, name: str, datetime: datetime, data_file: Path)[source]¶
Bases:
SerializableModel for only essential
Clipdata to be included inClipsIndex- classmethod from_clip(clip: Clip, root_dir: Path) Self[source]¶
Create an instance from a
Clip- Parameters:
root_dir (Path) – Relative parent directory of the
Clip.root_dir. This will typically be theClipsIndex.root_dir
- Return type:
- write_data(clip: Clip | ClipCollection, exist_ok: bool = False, indent: int | None = 2) None[source]¶
Serialize the clip data and save it to
data_file- Parameters:
clip (Clip | ClipCollection) – A
ClipCollectionorClipinstanceexist_ok (bool) – If
Falseand thedata_fileexists, it will not be overwritten and an exception will be raisedindent (int | None) – Indentation parameter to pass to
json.dumps()
- Return type:
None
- class granicus_archiver.clips.model.ClipsIndex(clips: dict[CLIP_ID, ClipIndex], root_dir: Path)[source]¶
Bases:
SerializableAn index of all clips containing a minimal amount of data
When serialized, this data will contain only basic clip information with relative paths to each clip’s full data representation.
This is intended for web services to use in order to avoid fetching a large amount of unnecessary data.
- root_dir: Path¶
Relative parent directory of the
ClipCollection.base_dir
- classmethod from_clip_collection(clips: ClipCollection, root_dir: PathLike) Self[source]¶
Create an instance from a
ClipCollection- Parameters:
clips (ClipCollection)
root_dir (PathLike)
- Return type:
- write_data(clip_collection: ClipCollection | None = None, exist_ok: bool = False, indent: int | None = 2) None[source]¶
Write the root index data and each
clip'sfull dataThe root index will be stored as “clip-index.json” within the
root_dir. All items inclipswill be flattened as alistof the serialized form ofClipIndex.- Parameters:
clip_collection (ClipCollection | None) – If provided, the
write_data()method will be called on each item inclipsusing the clip collection’s data. IfNone, only the root index data will be saved.exist_ok (bool) – If
Falseand the data file exists, it will not be overwritten and an exception will be raised. This will also be passed when callingClipIndex.write_data().indent (int | None) – Indentation parameter to pass to
json.dumps()
- Return type:
None