Source code for granicus_archiver.types
from __future__ import annotations
from typing import ClassVar, Any, Self
from abc import ABC, abstractmethod
import dataclasses
from dataclasses import dataclass
import datetime
from multidict import MultiMapping
from .utils import SHA1Hash
Headers = MultiMapping[str]|dict[str, str]
UTC = datetime.timezone.utc
[docs]
class Serializable(ABC):
@abstractmethod
def serialize(self) -> dict[str, Any]:
raise NotImplementedError
@classmethod
@abstractmethod
def deserialize(cls, data: dict[str, Any]) -> Self:
raise NotImplementedError
[docs]
@dataclass
class FileMeta(Serializable):
"""Metadata for a file
"""
content_length: int #: File size (in bytes)
content_type: str #: The file's mime type
last_modified: datetime.datetime|None #: Last modified datetime
etag: str|None #: The etag value (if available)
sha1: SHA1Hash|None = None #: SHA1 hash of the file
# Tue, 04 Jun 2024 00:22:54 GMT
dt_fmt: ClassVar[str] = '%a, %d %b %Y %H:%M:%S GMT'
[docs]
@classmethod
def from_headers(cls, headers: Headers) -> Self:
"""Create an instance from http headers
"""
dt_str = headers.get('Last-Modified')
if dt_str is not None:
dt = datetime.datetime.strptime(dt_str, cls.dt_fmt).replace(tzinfo=UTC)
else:
dt = None
etag = headers.get('Etag')
if etag is not None:
etag = etag.strip('"')
return cls(
content_length=int(headers.get('Content-Length', '0')),
content_type=headers['Content-Type'],
last_modified=dt,
etag=etag,
)
[docs]
@classmethod
def create_zero_length(cls) -> Self:
"""Create an instance to indicate that the file has a reported length
of zero
This may be used to indicate that a file is malformed or no longer
exists on the server.
Zero-length :class:`FileMeta` instances can be detected from their
:attr:`is_zero_length` attribute.
"""
return cls(
content_length=-1,
content_type='__none__',
last_modified=None,
etag=None,
)
@property
def is_pdf(self) -> bool:
"""Whether this is a pdf file
"""
return self.content_type == 'application/pdf'
@property
def is_zero_length(self) -> bool:
"""Whether this instance represents a zero-length file (created by
:meth:`create_zero_length`)
"""
return self.content_length == -1 and self.content_type == '__none__'
def serialize(self) -> dict[str, Any]:
d = dataclasses.asdict(self)
if self.last_modified is not None:
dt = self.last_modified.astimezone(UTC)
d['last_modified'] = dt.strftime(self.dt_fmt)
return d
@classmethod
def deserialize(cls, data: dict[str, Any]) -> Self:
kw = data.copy()
if kw['last_modified'] is not None:
dt = datetime.datetime.strptime(kw['last_modified'], cls.dt_fmt)
kw['last_modified'] = dt.replace(tzinfo=UTC)
return cls(**kw)