swegov-opendata 0.1.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.
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: swegov-opendata
3
+ Version: 0.1.0
4
+ Summary: Data models for handling Swedish riksdagens öppna data (open data)
5
+ Author: Språkbanken
6
+ Author-email: Språkbanken <sb-info@svenska.gu.se>
7
+ License-Expression: MIT
8
+ Requires-Dist: pydantic>=2.12.5
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+
12
+ # swegov-opendata
13
+
14
+ [![PyPI version](https://img.shields.io/pypi/v/swegov-opendata/0.1.0.svg)](https://pypi.org/project/swegov-opendata/)
15
+ [![PyPI license](https://img.shields.io/pypi/l/swegov-opendata/0.1.0.svg)](https://pypi.org/project/swegov-opendata/)
16
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/swegov-opendata/0.1.0.svg)](https://pypi.org/project/swegov-opendata)
17
+
18
+ [![Maturity badge - level 2](https://img.shields.io/badge/Maturity-Level%202%20--%20First%20Release-yellowgreen.svg)](https://github.com/spraakbanken/getting-started/blob/main/scorecard.md)
19
+ [![Stage](https://img.shields.io/pypi/status/swegov-opendata/0.1.0.svg)](https://pypi.org/project/swegov-opendata/)
20
+
21
+ [![codecov](https://codecov.io/gh/spraakbanken/swegov-opendata-py/graph/badge.svg?token=DUV4CL6AK2)](https://codecov.io/gh/spraakbanken/swegov-opendata-py)
22
+
23
+ [![CI(check)](https://github.com/spraakbanken/swegov-opendata-py/actions/workflows/check.yml/badge.svg)](https://github.com/spraakbanken/swegov-opendata-py/actions/workflows/check.yml)
24
+ [![CI(release)](https://github.com/spraakbanken/swegov-opendata-py/actions/workflows/release.yml/badge.svg)](https://github.com/spraakbanken/swegov-opendata-py/actions/workflows/release.yml)
25
+ [![CI(scheduled)](https://github.com/spraakbanken/swegov-opendata-py/actions/workflows/rolling.yml/badge.svg)](https://github.com/spraakbanken/swegov-opendata-py/actions/workflows/rolling.yml)
26
+ [![CI(test)](https://github.com/spraakbanken/swegov-opendata-py/actions/workflows/test.yml/badge.svg)](https://github.com/spraakbanken/swegov-opendata-py/actions/workflows/test.yml)
27
+
28
+ Pydantic models for [Riksdagens Öppna data](http://data.riksdagen.se/).
29
+
30
+ ## Install
31
+
32
+ Add this to your project with
33
+
34
+ ```shell
35
+ uv add swegov-opendata
36
+ ```
37
+
38
+ ## Minimum Supported Python Version Policy
39
+
40
+ The Minimum Supported Python Version is fixed for a given minor (1.x)
41
+ version. However it can be increased when bumping minor versions, i.e. going
42
+ from 1.0 to 1.1 allows us to increase the Minimum Supported Python Version. Users unable to increase their
43
+ Python version can use an older minor version instead. Below is a list of sparv-sbx-corpus-statistics versions
44
+ and their Minimum Supported Python Version:
45
+
46
+ - v0.1: Python 3.11.
47
+
48
+ Note however that sparv-sbx-corpus-statistics also has dependencies, which might have different MSRV
49
+ policies. We try to stick to the above policy when updating dependencies, but
50
+ this is not always possible.
51
+
52
+ ## Changelog
53
+
54
+ This project keeps a [changelog](https://github.com/spraakbanken/swegov-opendata-py/blob/0.1.0/CHANGELOG.md).
55
+
56
+ ## License
57
+
58
+ This repository is licensed under the [MIT](https://github.com/spraakbanken/swegov-opendata-py/blob/0.1.0/LICENSE) license.
@@ -0,0 +1,47 @@
1
+ # swegov-opendata
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/swegov-opendata/0.1.0.svg)](https://pypi.org/project/swegov-opendata/)
4
+ [![PyPI license](https://img.shields.io/pypi/l/swegov-opendata/0.1.0.svg)](https://pypi.org/project/swegov-opendata/)
5
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/swegov-opendata/0.1.0.svg)](https://pypi.org/project/swegov-opendata)
6
+
7
+ [![Maturity badge - level 2](https://img.shields.io/badge/Maturity-Level%202%20--%20First%20Release-yellowgreen.svg)](https://github.com/spraakbanken/getting-started/blob/main/scorecard.md)
8
+ [![Stage](https://img.shields.io/pypi/status/swegov-opendata/0.1.0.svg)](https://pypi.org/project/swegov-opendata/)
9
+
10
+ [![codecov](https://codecov.io/gh/spraakbanken/swegov-opendata-py/graph/badge.svg?token=DUV4CL6AK2)](https://codecov.io/gh/spraakbanken/swegov-opendata-py)
11
+
12
+ [![CI(check)](https://github.com/spraakbanken/swegov-opendata-py/actions/workflows/check.yml/badge.svg)](https://github.com/spraakbanken/swegov-opendata-py/actions/workflows/check.yml)
13
+ [![CI(release)](https://github.com/spraakbanken/swegov-opendata-py/actions/workflows/release.yml/badge.svg)](https://github.com/spraakbanken/swegov-opendata-py/actions/workflows/release.yml)
14
+ [![CI(scheduled)](https://github.com/spraakbanken/swegov-opendata-py/actions/workflows/rolling.yml/badge.svg)](https://github.com/spraakbanken/swegov-opendata-py/actions/workflows/rolling.yml)
15
+ [![CI(test)](https://github.com/spraakbanken/swegov-opendata-py/actions/workflows/test.yml/badge.svg)](https://github.com/spraakbanken/swegov-opendata-py/actions/workflows/test.yml)
16
+
17
+ Pydantic models for [Riksdagens Öppna data](http://data.riksdagen.se/).
18
+
19
+ ## Install
20
+
21
+ Add this to your project with
22
+
23
+ ```shell
24
+ uv add swegov-opendata
25
+ ```
26
+
27
+ ## Minimum Supported Python Version Policy
28
+
29
+ The Minimum Supported Python Version is fixed for a given minor (1.x)
30
+ version. However it can be increased when bumping minor versions, i.e. going
31
+ from 1.0 to 1.1 allows us to increase the Minimum Supported Python Version. Users unable to increase their
32
+ Python version can use an older minor version instead. Below is a list of sparv-sbx-corpus-statistics versions
33
+ and their Minimum Supported Python Version:
34
+
35
+ - v0.1: Python 3.11.
36
+
37
+ Note however that sparv-sbx-corpus-statistics also has dependencies, which might have different MSRV
38
+ policies. We try to stick to the above policy when updating dependencies, but
39
+ this is not always possible.
40
+
41
+ ## Changelog
42
+
43
+ This project keeps a [changelog](https://github.com/spraakbanken/swegov-opendata-py/blob/0.1.0/CHANGELOG.md).
44
+
45
+ ## License
46
+
47
+ This repository is licensed under the [MIT](https://github.com/spraakbanken/swegov-opendata-py/blob/0.1.0/LICENSE) license.
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "swegov-opendata"
3
+ version = "0.1.0"
4
+ description = "Data models for handling Swedish riksdagens öppna data (open data)"
5
+ readme = "README.md"
6
+ requires-python = ">= 3.11"
7
+ license = "MIT"
8
+ dependencies = ["pydantic>=2.12.5"]
9
+
10
+ [[project.authors]]
11
+ name = "Språkbanken"
12
+ email = "sb-info@svenska.gu.se"
13
+
14
+ [dependency-groups]
15
+ lint = [
16
+ "ruff>=0.16.9",
17
+ "ty>=0.0.84",
18
+ ]
19
+ test = [
20
+ "pytest>=9.0.3",
21
+ "pytest-cov>=7.0.0",
22
+ "syrupy>=6.0.0",
23
+ ]
24
+
25
+ [[dependency-groups.dev]]
26
+ include-group = "lint"
27
+
28
+ [[dependency-groups.dev]]
29
+ include-group = "test"
30
+
31
+ [build-system]
32
+ requires = ["uv_build>=0.12,<0.13"]
33
+ build-backend = "uv_build"
34
+
35
+ [tool.uv]
36
+ exclude-newer = "P7D"
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "swegov-opendata"
3
+ version = "0.1.0"
4
+ description = "Data models for handling Swedish riksdagens öppna data (open data)"
5
+ readme = "README.md"
6
+ requires-python = ">= 3.11"
7
+ license = "MIT"
8
+ authors = [
9
+ { name = "Språkbanken", email = "sb-info@svenska.gu.se" },
10
+ ]
11
+ dependencies = [
12
+ "pydantic>=2.12.5",
13
+ ]
14
+
15
+ [dependency-groups]
16
+ dev = [{ include-group = "lint" }, { include-group = "test" }]
17
+ lint = [
18
+ "ruff>=0.16.9",
19
+ "ty>=0.0.84",
20
+ ]
21
+ test = [
22
+ "pytest>=9.0.3",
23
+ "pytest-cov>=7.0.0",
24
+ "syrupy>=6.0.0",
25
+ ]
26
+
27
+ [build-system]
28
+ requires = ["uv_build>=0.12,<0.13"]
29
+ build-backend = "uv_build"
30
+
31
+ [tool.uv]
32
+ exclude-newer = "P7D"
File without changes
@@ -0,0 +1,24 @@
1
+ import datetime
2
+ import typing as t
3
+
4
+ import pydantic
5
+
6
+ from swegov_opendata import shared
7
+
8
+
9
+ class Aktivitet(pydantic.BaseModel):
10
+ datum: datetime.datetime
11
+ hangar_id: str | None = None
12
+ kod: str
13
+ namn: str
14
+ status: str | None
15
+ ordning: str
16
+ process: str | None
17
+
18
+ model_config = pydantic.ConfigDict(extra="forbid")
19
+
20
+
21
+ class DokAktivitet(pydantic.BaseModel):
22
+ aktivitet: t.Annotated[list[Aktivitet], pydantic.BeforeValidator(shared.ensure_list)]
23
+
24
+ model_config = pydantic.ConfigDict(extra="forbid")
@@ -0,0 +1,44 @@
1
+ import enum
2
+ import typing as t
3
+
4
+ import pydantic
5
+
6
+ from swegov_opendata import shared
7
+
8
+
9
+ class FilTyp(enum.StrEnum):
10
+ Doc = "doc"
11
+ Docx = "docx"
12
+ Htm = "htm"
13
+ Pdf = "pdf"
14
+
15
+ def __str__(self) -> str:
16
+ return self.value
17
+
18
+ def __repr__(self) -> str:
19
+ return str(self)
20
+
21
+ @classmethod
22
+ def _missing_(cls, value) -> t.Self:
23
+ for member in cls:
24
+ if member.value == value.lower():
25
+ return member
26
+ raise ValueError("Unknown value '{value}'")
27
+
28
+
29
+ class Bilaga(pydantic.BaseModel):
30
+ dok_id: str
31
+ titel: str | None = None
32
+ subtitel: str | None
33
+ filnamn: str
34
+ filstorlek: str
35
+ filtyp: FilTyp
36
+ fil_url: pydantic.HttpUrl
37
+
38
+ model_config = pydantic.ConfigDict(extra="forbid")
39
+
40
+
41
+ class DokBilaga(pydantic.BaseModel):
42
+ bilaga: t.Annotated[list[Bilaga], pydantic.BeforeValidator(shared.ensure_list)]
43
+
44
+ model_config = pydantic.ConfigDict(extra="forbid")
@@ -0,0 +1,59 @@
1
+ import enum
2
+ import xml.etree.ElementTree as ET
3
+ from dataclasses import dataclass
4
+ from xml.etree.ElementTree import Element
5
+
6
+ from pydantic import HttpUrl
7
+
8
+
9
+ class DataFormat(enum.StrEnum):
10
+ json = "json"
11
+ csv = "csv"
12
+ csvt = "csvt"
13
+ html = "html"
14
+ sql = "sql"
15
+ text = "text"
16
+ xml = "xml"
17
+
18
+
19
+ @dataclass
20
+ class Dataset:
21
+ format: DataFormat
22
+ url: HttpUrl
23
+ uppdaterad: str
24
+
25
+ @classmethod
26
+ def from_elem(cls, elem: Element) -> "Dataset":
27
+ if elem.tag != "dataset":
28
+ raise RuntimeError(f"Expected <dataset>, found <{elem.tag}>")
29
+ format_ = None
30
+ url = None
31
+ uppdaterad = None
32
+ for child in elem:
33
+ if child.tag == "format":
34
+ format_ = DataFormat(child.text)
35
+ elif child.tag == "url":
36
+ url = child.text
37
+ elif child.tag == "uppdaterad":
38
+ uppdaterad = child.text
39
+
40
+ if format_ is None:
41
+ raise RuntimeError("Dataset is missing 'format'")
42
+ if url is None:
43
+ raise RuntimeError("Dataset is missing 'url'")
44
+ if uppdaterad is None:
45
+ raise RuntimeError("Dataset is missing 'uppdaterad'")
46
+ return cls(format=format_, url=HttpUrl(url), uppdaterad=uppdaterad)
47
+
48
+
49
+ @dataclass
50
+ class DatasetLista:
51
+ datasets: list[Dataset]
52
+
53
+ @classmethod
54
+ def from_xml(cls, xml: str) -> "DatasetLista":
55
+ root = ET.fromstring(xml)
56
+ if root.tag != "datasetlista":
57
+ raise RuntimeError(f"Expected <datasetlista>, found <{root.tag}>")
58
+ datasets = [Dataset.from_elem(child) for child in root]
59
+ return cls(datasets=datasets)
@@ -0,0 +1,54 @@
1
+ import datetime
2
+ import typing as t
3
+
4
+ import pydantic
5
+
6
+ from swegov_opendata import shared
7
+
8
+
9
+ class Anforande(pydantic.BaseModel):
10
+ anf_beteckning: str | None
11
+ anf_datum: datetime.datetime
12
+ anf_hangar_id: t.Annotated[int | None, pydantic.BeforeValidator(shared.ensure_int_opt)]
13
+ anf_id: str | None = None
14
+ anf_klockslag: str | None
15
+ anf_nummer: str
16
+ anf_rm: str | None
17
+ anf_sekunder: int
18
+ anf_text: str | None = None
19
+ anf_typ: str | None
20
+ anf_video_id: str
21
+ datumtid: datetime.datetime
22
+ debatt_id: str | None = None
23
+ debatt_titel: str | None = None
24
+ debatt_typ: str | None = None
25
+ dok_beteckning: str | None = None
26
+ dok_id: str | None = None
27
+ dok_intressent: str | None = None
28
+ intressent_id: str | None = None
29
+ kon: str | None = None
30
+ parent_id: str | None = None
31
+ parti: str | None = None
32
+ startpos: int | None = None
33
+ systemdatum: datetime.datetime | None = None
34
+ talare: str | None
35
+ talare_kort: str | None = None
36
+ tumnagel: t.Annotated[list[str] | None, pydantic.BeforeValidator(shared.ensure_list_opt)] = (
37
+ None
38
+ )
39
+ tumnagel_mini: str | None = None
40
+ tumnagel_liten: str | None = None
41
+ tumnagel_medium: str | None = None
42
+ tumnagel_stor: str | None = None
43
+ video_id: str | None
44
+ video_url: str
45
+ videostatus: int | None = None
46
+ voteringspunkt: str | None = None
47
+
48
+ model_config = pydantic.ConfigDict(extra="forbid")
49
+
50
+
51
+ class Debatt(pydantic.BaseModel):
52
+ anforande: t.Annotated[list[Anforande], pydantic.BeforeValidator(shared.ensure_list)]
53
+
54
+ model_config = pydantic.ConfigDict(extra="forbid")
@@ -0,0 +1,235 @@
1
+ import datetime
2
+ import enum
3
+ import typing as t
4
+ from pathlib import Path
5
+
6
+ import pydantic
7
+
8
+ from swegov_opendata.dokument.aktivitet import DokAktivitet
9
+ from swegov_opendata.dokument.bilaga import DokBilaga
10
+ from swegov_opendata.dokument.debatt import Debatt
11
+ from swegov_opendata.dokument.forslag import (
12
+ DokForslag,
13
+ DokMotForslag,
14
+ DokUtskottsForslag,
15
+ )
16
+ from swegov_opendata.dokument.intressent import DokIntressent
17
+ from swegov_opendata.dokument.media import WebbMedia
18
+ from swegov_opendata.dokument.referens import DokReferens
19
+ from swegov_opendata.dokument.uppgift import DokUppgift
20
+
21
+ _DOKUMENT_TYP_MAP: dict[str, str] = {
22
+ "bet": "betänkande",
23
+ "dir": "kommittéedirektiv",
24
+ "ds": "departementsserien",
25
+ "eundok": "utskottsdokument",
26
+ "f-lista": "föredragningslista",
27
+ "fr": "skriftlig fråga",
28
+ "frs": "svar på skriftlig fråga",
29
+ "frsrdg": "framställningar",
30
+ "ip": "interpellation",
31
+ "kf-lista": "kallelser och föredragningslistor",
32
+ "KOM": "EU-förslag",
33
+ "komm": "kommittéeberättelser",
34
+ "prop": "proposition",
35
+ "prot": "protokoll",
36
+ "rfr": "rapporter från riksdagen",
37
+ "rir": "riksrevisionens granskningsrapporter",
38
+ "rskr": "riksdagsskrivelse",
39
+ "sou": "statens offentliga utredningar",
40
+ "t-lista": "talarlista",
41
+ "urf": "utredningar från Riksdagsförvaltningen",
42
+ "utskottsdokument": "utskottsdokument",
43
+ "yttr": "yttrande",
44
+ }
45
+
46
+
47
+ class DokumentTyp(enum.StrEnum):
48
+ Bet = "bet"
49
+ Diarie = "diarie"
50
+ Dir = "dir"
51
+ Ds = "ds"
52
+ EuDokument = "eu-dokument"
53
+ Eunbil = "eunbil"
54
+ Eundok = "eundok"
55
+ FLista = "f-lista"
56
+ Fpm = "fpm"
57
+ Fr = "fr"
58
+ Frs = "frs"
59
+ Frsrdg = "frsrdg"
60
+ Ip = "ip"
61
+ Kalakt = "kalakt"
62
+ Kammakt = "kammakt"
63
+ KfLista = "kf-lista"
64
+ Kom = "KOM"
65
+ Komm = "komm"
66
+ Mot = "mot"
67
+ Prop = "prop"
68
+ Prot = "prot"
69
+ Rfr = "rfr"
70
+ Rir = "rir"
71
+ Rskr = "rskr"
72
+ Samtr = "samtr"
73
+ Sfs = "sfs"
74
+ Skrsam = "skrsam"
75
+ Sou = "sou"
76
+ TLista = "t-lista"
77
+ Urf = "urf"
78
+ Utskottsdokument = "utskottsdokument"
79
+ Uttag = "uttag"
80
+ Yttr = "yttr"
81
+
82
+ @classmethod
83
+ def _missing_(cls, value) -> t.Self:
84
+ for member in cls:
85
+ if member.value == value.lower():
86
+ return member
87
+ raise ValueError(f"Unknown value '{value}'.")
88
+
89
+ def short_name(self) -> str:
90
+ return self.value
91
+
92
+ def long_name(self) -> str:
93
+ return _DOKUMENT_TYP_MAP.get(self.value) or self.value
94
+
95
+
96
+ def split_space_take_first(value: t.Any) -> t.Any:
97
+ if isinstance(value, str):
98
+ return "" if not value else value.split()[0]
99
+ return value
100
+
101
+
102
+ def ensure_none_if_empty(value: t.Any) -> t.Any:
103
+ if isinstance(value, str):
104
+ return None if value == "" else value
105
+ return value
106
+
107
+
108
+ def ensure_string(value: t.Any) -> t.Any:
109
+ def _ensure_str(v: t.Any) -> str:
110
+ return v if isinstance(v, str) else str(v)
111
+
112
+ if isinstance(value, list):
113
+ if len(value) == 0:
114
+ return ""
115
+ return _ensure_str(value[0])
116
+ return _ensure_str(value)
117
+
118
+
119
+ class Dokument(pydantic.BaseModel):
120
+ beteckning: str | None
121
+ datum: t.Annotated[
122
+ datetime.date | None,
123
+ pydantic.BeforeValidator(split_space_take_first),
124
+ pydantic.BeforeValidator(ensure_none_if_empty),
125
+ ]
126
+ debattnamn: str | None = None
127
+ dok_id: str
128
+ doktyp: t.Annotated[str, pydantic.BeforeValidator(ensure_string)]
129
+ dokument_url_html: pydantic.HttpUrl
130
+ dokument_url_text: pydantic.HttpUrl
131
+ dokumentnamn: str | None = None
132
+ dokumentstatus_url_xml: pydantic.HttpUrl
133
+ hangar_id: str
134
+ html: str | None = None
135
+ htmlformat: str | None
136
+ images: str | None = None
137
+ metadata: str | None = None
138
+ mottagare: str | None
139
+ nummer: str | None
140
+ organ: str | None
141
+ pretext: str | None = None
142
+ publicerad: t.Annotated[
143
+ datetime.datetime | None, pydantic.BeforeValidator(ensure_none_if_empty)
144
+ ]
145
+ relaterat_id: str | None
146
+ rm: str | None
147
+ rubriker: str | None = None
148
+ slutnummer: str | None
149
+ source: str | None
150
+ sourceid: str | None
151
+ status: str | None
152
+ subtitel: str | None
153
+ subtyp: str | None
154
+ # systemdatum: datetime.datetime
155
+ systemdatum: t.Annotated[
156
+ datetime.datetime | None,
157
+ pydantic.BeforeValidator(split_space_take_first),
158
+ pydantic.BeforeValidator(ensure_none_if_empty),
159
+ ]
160
+ tempbeteckning: str | None
161
+ text: str | None = None
162
+ titel: str | None
163
+ typ: t.Annotated[DokumentTyp | None, pydantic.BeforeValidator(ensure_none_if_empty)]
164
+ typrubrik: str | None = None
165
+ utskottsforslag_url_xml: str | None = None
166
+
167
+ model_config = pydantic.ConfigDict(extra="forbid")
168
+
169
+ def get_rm(self) -> str:
170
+ return self.rm or "missing"
171
+
172
+ def get_typ_short_name(self) -> str:
173
+ return self.typ.short_name() if self.typ else "missing"
174
+
175
+ def get_typ_long_name(self) -> str:
176
+ return self.typ.long_name() if self.typ else "missing"
177
+
178
+
179
+ class DokumentStatus(pydantic.BaseModel):
180
+ debatt: Debatt | None = None
181
+ dokaktivitet: DokAktivitet | None = None
182
+ dokbilaga: DokBilaga | None = None
183
+ dokforslag: DokForslag | None = None
184
+ dokintressent: DokIntressent | None = None
185
+ dokmotforslag: DokMotForslag | None = None
186
+ dokreferens: DokReferens | None = None
187
+ dokument: Dokument
188
+ dokuppgift: DokUppgift | None = None
189
+ dokutskottsforslag: DokUtskottsForslag | None = None
190
+ webbmedia: WebbMedia | None = None
191
+
192
+ model_config = pydantic.ConfigDict(extra="forbid")
193
+
194
+
195
+ class DokumentStatusPage(pydantic.BaseModel):
196
+ dokumentstatus: DokumentStatus | None
197
+
198
+ @classmethod
199
+ def deserialize_from_str(cls, src: str | bytes | bytearray) -> "DokumentStatusPage":
200
+ try:
201
+ return DokumentStatusPage.model_validate_json(src)
202
+ except pydantic.ValidationError as exc:
203
+ print(f"error type: '{exc.errors()[0]['type']}'")
204
+ exc_msg = exc.errors()[0]["msg"]
205
+ print(f"error msg: '{exc_msg}'")
206
+ print(f"{exc=}")
207
+ if exc.errors()[0]["type"] != "json_invalid":
208
+ raise
209
+ column = int(exc_msg.split()[-1])
210
+ print(f"{src[column - 50:column + 50]=}")
211
+ new_src: str = src if isinstance(src, str) else src.decode("utf-8")
212
+ new_src = (
213
+ new_src.replace("{/* RESERVATIONSTEXT */}", r'""')
214
+ .replace("/* RESERVATIONSTEXT */", "")
215
+ .replace('\\"', "TEMPSLASH")
216
+ .replace(r"\"", '"')
217
+ .replace("TEMPSLASH", r"\"")
218
+ .replace(r"\\r\\n", r"\r\n")
219
+ .replace('"p":}', '"p":""}')
220
+ )
221
+ try:
222
+ return DokumentStatusPage.model_validate_json(new_src)
223
+ except pydantic.ValidationError as exc:
224
+ dt = datetime.datetime.now(tz=datetime.UTC).strftime("%Y-%m-%dT%H%M%S")
225
+ Path(f"new_src-{dt}.json").write_text(new_src)
226
+ print(f"error type: '{exc.errors()[0]['type']}'")
227
+ exc_msg = exc.errors()[0]["msg"]
228
+ print(f"error msg: '{exc_msg}'")
229
+ print(f"{exc=}")
230
+ if exc.errors()[0]["type"] != "json_invalid":
231
+ raise
232
+ column = int(exc_msg.split()[-1])
233
+ print(f"{new_src[column - 50:column + 50]=}")
234
+ # new_src = src.replace("{/* RESERVATIONSTEXT */}", "")
235
+ raise
@@ -0,0 +1,147 @@
1
+ import datetime
2
+ import typing as t
3
+
4
+ import pydantic
5
+
6
+ from swegov_opendata import shared
7
+ from swegov_opendata.dokument.dokument import DokumentTyp
8
+
9
+
10
+ class Fil(pydantic.BaseModel):
11
+ typ: str
12
+ namn: str
13
+ storlek: int
14
+ url: str
15
+
16
+ model_config = pydantic.ConfigDict(extra="forbid")
17
+
18
+
19
+ class FilBilaga(pydantic.BaseModel):
20
+ fil: t.Annotated[list[Fil] | None, pydantic.BeforeValidator(shared.ensure_list_opt)]
21
+
22
+ model_config = pydantic.ConfigDict(extra="forbid")
23
+
24
+
25
+ class SokData(pydantic.BaseModel):
26
+ titel: str
27
+ undertitel: str
28
+ soktyp: str | None
29
+ statusrad: str
30
+ brodsmula: str | None
31
+ parti_kod: str | None
32
+ parti_namn: str | None
33
+ parti_website_url: str | None
34
+ parti_website_namn: str | None
35
+ parti_epost: str | None
36
+ parti_telefon: str | None
37
+ parti_telefontider: str | None
38
+ parti_logotyp_img_id: str | None
39
+ parti_logotyp_img_url: str | None
40
+ parti_logotyp_img_alt: str | None
41
+ parti_mandat: str | None
42
+ kalenderprio: str | None
43
+
44
+ model_config = pydantic.ConfigDict(extra="forbid")
45
+
46
+
47
+ class DokumentListaDokument(pydantic.BaseModel):
48
+ ardometyp: str | None
49
+ audio: str | None
50
+ avdelning: str
51
+ avdelningar: shared.Avdelningar | None
52
+ beredningsdag: str | None
53
+ beslutad: str | None
54
+ beslutsdag: str | None
55
+ beteckning: str | None
56
+ database: str
57
+ datum: datetime.date
58
+ debatt: str | None
59
+ debattdag: str | None
60
+ debattgrupp: str | None
61
+ debattnamn: str
62
+ debattsekunder: str | None
63
+ dok_id: str
64
+ dokintressent: str | None
65
+ doktyp: str
66
+ dokument_url_html: str
67
+ dokument_url_text: str
68
+ dokumentformat: str | None
69
+ dokumentnamn: str
70
+ domain: str
71
+ filbilaga: FilBilaga | None
72
+ id: str
73
+ inlamnad: str | None
74
+ justeringsdag: str | None
75
+ kall_id: str | None
76
+ kalla: str | None
77
+ klockslag: str | None
78
+ lang: str | None
79
+ motionstid: str | None
80
+ notis: str | None
81
+ notisrubrik: str
82
+ nummer: str | None
83
+ organ: str | None
84
+ plats: str | None
85
+ # TODO this field can contain date (2018-03-07) and datetime (2016-02-11 15:28:15)
86
+ publicerad: str
87
+ rddata: str | None
88
+ rdrest: str | None
89
+ relaterat_id: str | None
90
+ relurl: str | None
91
+ reservationer: str | None
92
+ rm: str
93
+ score: str
94
+ slutdatum: str | None
95
+ sokdata: SokData
96
+ status: str | None
97
+ struktur: str | None
98
+ subtyp: str
99
+ summary: str | None
100
+ systemdatum: datetime.datetime
101
+ tempbeteckning: str | None
102
+ tilldelat: str | None
103
+ titel: str
104
+ traff: int
105
+ typ: DokumentTyp
106
+ undertitel: str | None
107
+ url: str | None
108
+ video: str | None
109
+
110
+ model_config = pydantic.ConfigDict(extra="forbid")
111
+
112
+ def get_rm(self) -> str:
113
+ return self.rm or "missing"
114
+
115
+ def get_typ_short_name(self) -> str:
116
+ return self.typ.short_name() if self.typ else "missing"
117
+
118
+ def get_typ_long_name(self) -> str:
119
+ return self.typ.long_name() if self.typ else "missing"
120
+
121
+
122
+ class DokumentLista(pydantic.BaseModel):
123
+ d_dt: str = pydantic.Field(alias="@dDt")
124
+ d_pre: str = pydantic.Field(alias="@dPre")
125
+ d_r: str = pydantic.Field(alias="@dR")
126
+ d_sol: str = pydantic.Field(alias="@dSol")
127
+ datum: datetime.datetime = pydantic.Field(alias="@datum")
128
+ ms: str = pydantic.Field(alias="@ms")
129
+ nasta_sida: str | None = pydantic.Field(alias="@nasta_sida")
130
+ q: str = pydantic.Field(alias="@q")
131
+ sida: int = pydantic.Field(alias="@sida")
132
+ sidor: int = pydantic.Field(alias="@sidor")
133
+ traff_fran: int = pydantic.Field(alias="@traff_fran")
134
+ traff_till: int = pydantic.Field(alias="@traff_till")
135
+ traffar: int = pydantic.Field(alias="@traffar")
136
+ varning: str | None = pydantic.Field(None, alias="@varning")
137
+ version: str = pydantic.Field(alias="@version")
138
+ facettlista: str | None
139
+ dokument: list[DokumentListaDokument]
140
+
141
+ model_config = pydantic.ConfigDict(extra="forbid")
142
+
143
+
144
+ class DokumentListaPage(pydantic.BaseModel):
145
+ dokumentlista: DokumentLista
146
+
147
+ model_config = pydantic.ConfigDict(extra="forbid")
@@ -0,0 +1,73 @@
1
+ import typing as t
2
+
3
+ import pydantic
4
+
5
+ from swegov_opendata import shared
6
+
7
+
8
+ class Forslag(pydantic.BaseModel):
9
+ hangar_id: str | None = None
10
+ nummer: int
11
+ beteckning: str | None
12
+ lydelse: t.Annotated[str, pydantic.BeforeValidator(shared.ensure_str)]
13
+ lydelse2: str | None
14
+ utskottet: str | None
15
+ kammaren: str | None
16
+ behandlas_i: str | None
17
+ behandlas_i_punkt: str | None = None
18
+ kammarbeslutstyp: str | None
19
+ intressent: str | None = None
20
+ avsnitt: str | None = None
21
+ grundforfattning: str | None = None
22
+ andringsforfattning: str | None = None
23
+
24
+ model_config = pydantic.ConfigDict(extra="forbid")
25
+
26
+
27
+ class DokForslag(pydantic.BaseModel):
28
+ forslag: t.Annotated[list[Forslag], pydantic.BeforeValidator(shared.ensure_list)]
29
+
30
+ model_config = pydantic.ConfigDict(extra="forbid")
31
+
32
+
33
+ class MotForslag(pydantic.BaseModel):
34
+ nummer: int
35
+ rubrik: str | None = None
36
+ forslag: str | None = None
37
+ partier: str | None
38
+ typ: str
39
+ utskottsforslag_punkt: int
40
+ id: str | None = None
41
+
42
+
43
+ class DokMotForslag(pydantic.BaseModel):
44
+ motforslag: t.Annotated[list[MotForslag], pydantic.BeforeValidator(shared.ensure_list)]
45
+
46
+
47
+ class UtskottsForslag(pydantic.BaseModel):
48
+ punkt: int
49
+ rubrik: str | None
50
+ forslag: str | None = None
51
+ forslag_del2: str | None = None
52
+ beteckning: str | None = None
53
+ beslut: str | None = None
54
+ beslutstyp: str | None = None
55
+ motforslag_nummer: int
56
+ motforslag_partier: str | None = None
57
+ votering_id: str | None = None
58
+ votering_sammanfattning_html: dict[str, t.Any] | None = None
59
+ votering_ledamot_url_xml: str | None = None
60
+ votering_url_xml: str | None = None
61
+ rm: str
62
+ bet: str
63
+ vinnare: str | None = None
64
+ voteringskrav: str | None = None
65
+ beslutsregelkvot: str | None = None
66
+ beslutsregelparagraf: str | None = None
67
+ punkttyp: str | None = None
68
+
69
+
70
+ class DokUtskottsForslag(pydantic.BaseModel):
71
+ utskottsforslag: t.Annotated[
72
+ list[UtskottsForslag], pydantic.BeforeValidator(shared.ensure_list)
73
+ ]
@@ -0,0 +1,19 @@
1
+ import typing as t
2
+
3
+ import pydantic
4
+
5
+ from swegov_opendata import shared
6
+
7
+
8
+ class Intressent(pydantic.BaseModel):
9
+ roll: str | None
10
+ namn: str | None
11
+ partibet: str | None
12
+ intressent_id: str | None
13
+ ordning: str
14
+
15
+ model_config = pydantic.ConfigDict(extra="forbid")
16
+
17
+
18
+ class DokIntressent(pydantic.BaseModel):
19
+ intressent: t.Annotated[list[Intressent], pydantic.BeforeValidator(shared.ensure_list)]
@@ -0,0 +1,33 @@
1
+ import datetime
2
+ import typing as t
3
+
4
+ import pydantic
5
+
6
+ from swegov_opendata import shared
7
+
8
+
9
+ class Media(pydantic.BaseModel):
10
+ dok_id: str
11
+ url: str
12
+ videofileurl: str
13
+ audiofileurl: str | None
14
+ downloadurl: str | None
15
+ thumbnailurl: str | None
16
+ debateurl: str
17
+ debattsekunder: int
18
+ debatt_rm: str | None
19
+ debatt_typ: str | None
20
+ dok_beteckning: str | None
21
+ datum: datetime.datetime | None
22
+ videostatus: int
23
+ source: str
24
+ inspelningstyp: str
25
+ systemdatum: datetime.datetime
26
+
27
+ model_config = pydantic.ConfigDict(extra="forbid")
28
+
29
+
30
+ class WebbMedia(pydantic.BaseModel):
31
+ media: t.Annotated[list[Media], pydantic.BeforeValidator(shared.ensure_list)]
32
+
33
+ model_config = pydantic.ConfigDict(extra="forbid")
@@ -0,0 +1,23 @@
1
+ import typing as t
2
+
3
+ import pydantic
4
+
5
+ from swegov_opendata import shared
6
+
7
+
8
+ class Referens(pydantic.BaseModel):
9
+ hangar_id: str | None = None
10
+ referenstyp: str | None
11
+ uppgift: str | None
12
+ ref_dok_id: str
13
+ ref_dok_typ: str
14
+ ref_dok_rm: str | None
15
+ ref_dok_bet: str | None
16
+ ref_dok_titel: str | None = pydantic.Field(default="")
17
+ ref_dok_subtitel: str | None
18
+ ref_dok_subtyp: str | None = None
19
+ ref_dok_dokumentnamn: str | None = None
20
+
21
+
22
+ class DokReferens(pydantic.BaseModel):
23
+ referens: t.Annotated[list[Referens], pydantic.BeforeValidator(shared.ensure_list)]
@@ -0,0 +1,22 @@
1
+ import datetime
2
+ import typing as t
3
+
4
+ import pydantic
5
+
6
+ from swegov_opendata import shared
7
+
8
+
9
+ class Uppgift(pydantic.BaseModel):
10
+ systemdatum: datetime.datetime | None = None
11
+ dok_id: str | None = None
12
+ kod: str
13
+ namn: str
14
+ text: str | None
15
+
16
+ model_config = pydantic.ConfigDict(extra="forbid")
17
+
18
+
19
+ class DokUppgift(pydantic.BaseModel):
20
+ uppgift: t.Annotated[list[Uppgift], pydantic.BeforeValidator(shared.ensure_list)]
21
+
22
+ model_config = pydantic.ConfigDict(extra="forbid")
@@ -0,0 +1,34 @@
1
+ import pydantic
2
+
3
+
4
+ class Votering(pydantic.BaseModel):
5
+ rm: str
6
+ beteckning: str
7
+ hangar_id: str
8
+ votering_id: str
9
+ punkt: str
10
+ namn: str
11
+ intressent_id: str
12
+ parti: str
13
+ valkrets: str | None
14
+ valkretsnummer: str
15
+ iort: str | None
16
+ rost: str
17
+ avser: str
18
+ votering: str
19
+ banknummer: str
20
+ fornamn: str
21
+ efternamn: str
22
+ kon: str
23
+ fodd: str
24
+ datum: str | None
25
+
26
+ model_config = pydantic.ConfigDict(extra="forbid")
27
+
28
+
29
+ class DokVotering(pydantic.BaseModel):
30
+ votering: list[Votering]
31
+
32
+
33
+ class DokVoteringPage(pydantic.BaseModel):
34
+ dokvotering: DokVotering | None
@@ -0,0 +1,56 @@
1
+ import typing as t
2
+
3
+ import pydantic
4
+
5
+
6
+ def ensure_int_opt(value: t.Any) -> int | None:
7
+ if value == "":
8
+ return None
9
+ return value
10
+
11
+
12
+ def ensure_list(value: t.Any) -> list[t.Any]:
13
+ if not isinstance(value, list):
14
+ return [value]
15
+ return value
16
+
17
+
18
+ def ensure_list_opt(value: t.Any) -> list[t.Any] | None:
19
+ if value is None:
20
+ return None
21
+ return ensure_list(value)
22
+
23
+
24
+ def ensure_str(value: t.Any) -> str:
25
+ if value is None:
26
+ return ""
27
+ if isinstance(value, str):
28
+ return value
29
+ if isinstance(value, list):
30
+ if len(value) == 0:
31
+ return ""
32
+ if len(value) == 1:
33
+ return ensure_str(value[0])
34
+ nonempty_values: set[str] = {v for v in value if isinstance(v, str) and len(v) > 0}
35
+ if len(nonempty_values) == 0:
36
+ return ""
37
+ if len(nonempty_values) == 1:
38
+ return next(iter(nonempty_values))
39
+ raise ValueError(f"Too many unique values: {list(nonempty_values)}")
40
+
41
+ return str(value)
42
+
43
+
44
+ def ensure_str_opt(value: t.Any) -> str | None:
45
+ if value is None:
46
+ return None
47
+
48
+ return ensure_str(value)
49
+
50
+
51
+ class Avdelning(pydantic.BaseModel):
52
+ text: str
53
+
54
+
55
+ class Avdelningar(pydantic.BaseModel):
56
+ avdelning: list[str]