Open Data API Design: Publikování a konzumace veřejných dat
Jak navrhnout a implementovat API pro otevřená data veřejné správy: DCAT-AP_IT, CKAN, standardy REST API s stránkováním a ukládáním do mezipaměti, otevřenými datovými portály a osvědčenými postupy pro kvalitu datové sady.
Kontext: Otevřená data jako veřejná infrastruktura
Otevřená data v italské veřejné správě nejsou jen principem transparentnosti: je to povinnost regulační (legislativní vyhláška 36/2006, novelizovaná legislativní vyhláškou 200/2021 při transpozici směrnice EU o otevřených datech 2019/1024), hnací silou ekonomických inovací a klíčovou složkou ekosystému Digitální platforma Národní údaje (PDND).
Národní portál data.gov.it, spravovaný AgID, shromažďuje a indexuje více než 5 000 datových sad Italské veřejné správy. Všechna zveřejněná data – od jízdních řádů veřejné dopravy až po výsledky volby, od údajů o kvalitě ovzduší po obecní usnesení — musí splňovat standardy z Přesná metadatace, kvalita a dostupnost.
Výzva veřejných otevřených dat je pro vývojáře dvojí: na jedné straně publikovat data aby byly skutečně znovu použitelné (surový CSV na institucionálním webu nestačí), na druhé straně konzumovat heterogenní data z různých zdrojů s proměnlivými standardy a kvalitou. Tento článek se zabývá oběma dimenzemi.
Co se naučíte
- Standard DCAT-AP_IT: struktura katalogu, datová sada, distribuce a povinná metadata
- CKAN: konfigurace, italská rozšíření a REST API pro správu datových sad
- Návrh REST API pro otevřená data: stránkování, filtrování, verzování a ukládání do mezipaměti
- Formáty nasazení: CSV, JSON-LD, RDF, GeoJSON, Parkety
- Kvalita dat: ověřování, profilování a metriky kvality DCAT
- Spotřeba otevřených dat: robustní klienti, zpracování chyb a normalizace
- PDND a interoperabilita: publikovat a využívat PA API prostřednictvím národní platformy
Standard DCAT-AP_IT: Metadatování veřejných dat
Italský profil DCAT-AP (Data Catalog Vocabulary Application Profile), známý jako DCAT-AP_IT, je zlatým standardem pro publikování metadat datové sady v italských PA. Je definován pomocí AgID a je založen na specifikacích W3C DCAT se specifickými rozšířeními pro italský kontext (geografie, licence, témata EUROVOC atd.).
Hlavní struktura DCAT-AP_IT zahrnuje tři základní entity:
- Katalog (dcat:Catalog): katalog datových sad PA. Obsahuje metadata o organizaci která zveřejňuje data, výchozí licenci, domovskou stránku a dobu aktualizace.
- Datové sady (dcat:Datasets): informační zdroj. Zahrnuje název, popis, témata, frekvenci datum aktualizace, datum vytvoření/úpravy, autor, čas a zeměpisná reference a konkrétní licence.
- Distribuce (dcat:Distribution): Konkrétní formát, ve kterém je datová sada k dispozici (CSV, JSON, RDF, shapefile atd.). Zahrnuje adresu URL ke stažení, formát, velikost, datum poslední úpravy.
# Generatore di metadati DCAT-AP_IT in Python
# Produce RDF/Turtle compatibile con dati.gov.it
from rdflib import Graph, Literal, URIRef, Namespace
from rdflib.namespace import DCAT, DCT, FOAF, RDF, XSD
from datetime import datetime, date
# Namespace italiani
DCATAPIT = Namespace("http://dati.gov.it/onto/dcatapit#")
VCARD = Namespace("http://www.w3.org/2006/vcard/ns#")
SKOS = Namespace("http://www.w3.org/2004/02/skos/core#")
def create_dcat_ap_it_metadata(
catalog_uri: str,
dataset_id: str,
title_it: str,
description_it: str,
publisher_name: str,
publisher_uri: str,
themes: list,
license_uri: str,
distributions: list
) -> str:
"""
Genera metadati DCAT-AP_IT completi in formato Turtle.
"""
g = Graph()
g.bind("dcat", DCAT)
g.bind("dct", DCT)
g.bind("foaf", FOAF)
g.bind("dcatapit", DCATAPIT)
# Definisci il Dataset
dataset_uri = URIRef(f"{catalog_uri}/dataset/{dataset_id}")
g.add((dataset_uri, RDF.type, DCAT.Dataset))
g.add((dataset_uri, RDF.type, DCATAPIT.Dataset))
# Metadati obbligatori
g.add((dataset_uri, DCT.title, Literal(title_it, lang="it")))
g.add((dataset_uri, DCT.description, Literal(description_it, lang="it")))
g.add((dataset_uri, DCT.modified, Literal(datetime.utcnow().date().isoformat(), datatype=XSD.date)))
g.add((dataset_uri, DCT.accrualPeriodicity, URIRef("http://publications.europa.eu/resource/authority/frequency/MONTHLY")))
g.add((dataset_uri, DCT.license, URIRef(license_uri)))
# Publisher (obbligatorio in DCAT-AP_IT)
publisher = URIRef(publisher_uri)
g.add((publisher, RDF.type, DCATAPIT.Agent))
g.add((publisher, FOAF.name, Literal(publisher_name, lang="it")))
g.add((publisher, DCT.identifier, Literal(publisher_uri)))
g.add((dataset_uri, DCT.publisher, publisher))
# Temi dal vocabolario EU EUROVOC
for theme_uri in themes:
g.add((dataset_uri, DCAT.theme, URIRef(theme_uri)))
# Distribuzione per ogni formato
for i, dist in enumerate(distributions):
dist_uri = URIRef(f"{dataset_uri}/distribution/{i}")
g.add((dist_uri, RDF.type, DCAT.Distribution))
g.add((dist_uri, RDF.type, DCATAPIT.Distribution))
g.add((dist_uri, DCAT.accessURL, URIRef(dist["url"])))
g.add((dist_uri, DCT.format, URIRef(f"http://publications.europa.eu/resource/authority/file-type/{dist['format']}")))
g.add((dist_uri, DCT.license, URIRef(license_uri)))
if "bytes" in dist:
g.add((dist_uri, DCAT.byteSize, Literal(dist["bytes"], datatype=XSD.decimal)))
g.add((dataset_uri, DCAT.distribution, dist_uri))
return g.serialize(format="turtle")
# Esempio di utilizzo
rdf_metadata = create_dcat_ap_it_metadata(
catalog_uri="https://dati.comune.milano.it",
dataset_id="qualità-aria-2024",
title_it="Qualità dell'Aria - Rilevazioni 2024",
description_it="Dataset con le rilevazioni orarie dei sensori di qualità dell'aria nel Comune di Milano",
publisher_name="Comune di Milano",
publisher_uri="http://spcdata.digitpa.gov.it/browse/page/Amministrazione/agid",
themes=["http://publications.europa.eu/resource/authority/data-theme/ENVI"],
license_uri="https://creativecommons.org/licenses/by/4.0/",
distributions=[
{"url": "https://dati.comune.milano.it/dataset/aria-2024.csv", "format": "CSV"},
{"url": "https://dati.comune.milano.it/dataset/aria-2024.json", "format": "JSON", "bytes": 45230000},
]
)
CKAN: Portál otevřených dat italské PA
CKAN (Comprehensive Knowledge Archive Network) je nejrozšířenější open source platforma pro správu vládních portálů s otevřenými daty. samotná data.gov.it je postavena na CKAN a to samé architekturu využívají desítky italských obcí, regionů a ministerstev.
Rozšíření ckanext-dcatapit, vyvinuté GeoSolutions a autonomní provincií Trento, přidává plnou podporu profilu DCAT-AP_IT, což umožňuje CKAN vystavovat a využívat kompatibilní metadata podle italských a evropských norem.
# Utilizzo delle API CKAN di dati.gov.it
# Le API CKAN sono REST con risposta JSON standardizzata
import httpx
import asyncio
from typing import Optional, List
class CKANClient:
def __init__(self, base_url: str, api_key: Optional[str] = None):
self.base_url = base_url.rstrip("/")
self.headers = {"Content-Type": "application/json"}
if api_key:
self.headers["Authorization"] = api_key
async def search_datasets(
self,
query: str,
filters: Optional[dict] = None,
rows: int = 20,
start: int = 0
) -> dict:
"""
Cerca dataset nel catalogo CKAN con filtraggio avanzato.
L'API usa Solr internamente per la ricerca full-text.
"""
params = {
"q": query,
"rows": rows,
"start": start,
}
# Filtri Solr per raffinamento
if filters:
fq_parts = [f"{k}:{v}" for k, v in filters.items()]
params["fq"] = " AND ".join(fq_parts)
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base_url}/api/3/action/package_search",
params=params,
headers=self.headers,
timeout=30.0
)
response.raise_for_status()
result = response.json()
if not result.get("success"):
raise ValueError(f"CKAN API error: {result.get('error')}")
return {
"total": result["result"]["count"],
"datasets": result["result"]["results"],
"page": start // rows + 1,
"per_page": rows
}
async def get_dataset(self, dataset_id: str) -> dict:
"""Recupera un dataset specifico con tutte le sue distribuzioni."""
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base_url}/api/3/action/package_show",
params={"id": dataset_id},
headers=self.headers,
timeout=30.0
)
response.raise_for_status()
result = response.json()
if not result.get("success"):
raise ValueError(f"Dataset not found: {dataset_id}")
return result["result"]
async def create_dataset(self, dataset_metadata: dict) -> dict:
"""Pubblica un nuovo dataset (richiede API key con permessi di scrittura)."""
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.base_url}/api/3/action/package_create",
json=dataset_metadata,
headers=self.headers,
timeout=60.0
)
response.raise_for_status()
return response.json()["result"]
# Utilizzo pratico
async def main():
client = CKANClient("https://www.dati.gov.it")
# Cerca dataset ambientali aggiornati
results = await client.search_datasets(
query="qualità aria",
filters={"res_format": "CSV", "groups": "ambiente"},
rows=10
)
print(f"Trovati {results['total']} dataset")
for ds in results["datasets"]:
print(f"- {ds['title']} ({ds['num_resources']} risorse)")
asyncio.run(main())
Návrh REST API pro otevřená data
Když PA publikuje svá data přes REST API (nejen přes CKAN), musí dodržovat principy návrhu které zaručují použitelnost, stabilitu a škálovatelnost. The Pokyny pro interoperabilitu PA technika AgID definují vzorce REST, které se mají dodržovat pro veřejné služby.
Stránkování a filtrování
# FastAPI: REST API per open data con paginazione conforme AgID
from fastapi import FastAPI, Query, HTTPException
from fastapi.responses import JSONResponse
from typing import Optional, List
from datetime import date
import math
app = FastAPI(
title="Open Data API - PA Example",
description="API per la pubblicazione di dati aperti - conforme Linee Guida AgID",
version="1.0.0"
)
@app.get("/api/v1/datasets/air-quality",
summary="Rilevazioni qualità aria",
tags=["Environmental Data"],
response_model=dict)
async def get_air_quality(
# Paginazione standard AgID: page + page_size
page: int = Query(default=1, ge=1, description="Numero pagina (da 1)"),
page_size: int = Query(default=100, ge=1, le=1000, description="Elementi per pagina (max 1000)"),
# Filtraggio
station_id: Optional[str] = Query(default=None, description="ID stazione di rilevamento"),
pollutant: Optional[str] = Query(default=None, description="Inquinante (PM2.5, PM10, NO2, O3)"),
date_from: Optional[date] = Query(default=None, description="Data inizio (ISO 8601)"),
date_to: Optional[date] = Query(default=None, description="Data fine (ISO 8601)"),
# Ordinamento
sort_by: str = Query(default="timestamp", description="Campo di ordinamento"),
sort_order: str = Query(default="desc", regex="^(asc|desc)$"),
# Formato output
format: str = Query(default="json", regex="^(json|csv|geojson)$")
):
"""
Restituisce le rilevazioni di qualità dell'aria con paginazione e filtraggio.
Supporta output in JSON, CSV e GeoJSON per compatibilità massima.
Conforme a DCAT-AP_IT e Linee Guida interoperabilità AgID.
"""
# Query al database con parametri
offset = (page - 1) * page_size
records, total_count = await air_quality_service.get_records(
station_id=station_id,
pollutant=pollutant,
date_from=date_from,
date_to=date_to,
sort_by=sort_by,
sort_order=sort_order,
limit=page_size,
offset=offset
)
total_pages = math.ceil(total_count / page_size)
# Risposta con metadati di paginazione (pattern AgID)
response_body = {
"data": records,
"meta": {
"total_count": total_count,
"page": page,
"page_size": page_size,
"total_pages": total_pages,
"has_next": page < total_pages,
"has_prev": page > 1,
},
"links": {
"self": f"/api/v1/datasets/air-quality?page={page}&page_size={page_size}",
"first": f"/api/v1/datasets/air-quality?page=1&page_size={page_size}",
"last": f"/api/v1/datasets/air-quality?page={total_pages}&page_size={page_size}",
"next": f"/api/v1/datasets/air-quality?page={page+1}&page_size={page_size}" if page < total_pages else None,
"prev": f"/api/v1/datasets/air-quality?page={page-1}&page_size={page_size}" if page > 1 else None,
},
"dataset": {
"id": "aria-qualità-2024",
"title": "Qualità dell'Aria",
"license": "CC BY 4.0",
"publisher": "Comune di Milano",
"last_updated": "2024-12-01"
}
}
# Content negotiation: JSON vs CSV vs GeoJSON
if format == "csv":
return Response(
content=records_to_csv(records),
media_type="text/csv",
headers={"Content-Disposition": "attachment; filename=aria-qualità.csv"}
)
elif format == "geojson":
return Response(
content=records_to_geojson(records),
media_type="application/geo+json"
)
return JSONResponse(content=response_body)
Ukládání do mezipaměti a výkon pro Open Data API
Veřejná data se ze své podstaty mění s předvídatelnou frekvencí (denně, týdně, měsíčně). Toto z nich dělá ideální kandidáty pro agresivní strategie ukládání do mezipaměti. Dobře navržené API pro otevřená data používá HTTP hlavičky standard pro ukládání do mezipaměti a dokáže obsloužit velkou většinu požadavků, aniž byste se dotkli databáze.
# Strategia di caching per open data API con Redis
from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
import redis.asyncio as redis
import json
import hashlib
from datetime import timedelta
class OpenDataCacheMiddleware:
"""
Middleware di caching per API open data.
Usa Redis come cache layer con TTL basato sulla frequenza di aggiornamento del dataset.
"""
# TTL per tipo di dataset (in secondi)
DATASET_TTL = {
"realtime": 60, # Dati real-time (qualità aria, traffico)
"daily": 86400, # Aggiornamento giornaliero
"weekly": 604800, # Aggiornamento settimanale
"monthly": 2592000, # Aggiornamento mensile
"static": 31536000, # Dati statici (confini amministrativi)
}
def __init__(self, app, redis_url: str):
self.app = app
self.redis = redis.from_url(redis_url)
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
request = Request(scope, receive)
# Cache solo GET requests
if request.method != "GET":
await self.app(scope, receive, send)
return
# Genera cache key da URL + query params (ordinati per consistenza)
cache_key = self._generate_cache_key(str(request.url))
# Cerca nella cache
cached = await self.redis.get(cache_key)
if cached:
response_data = json.loads(cached)
response = Response(
content=response_data["body"],
status_code=response_data["status_code"],
headers={
**response_data["headers"],
"X-Cache": "HIT",
"Cache-Control": "public, max-age=3600"
}
)
await response(scope, receive, send)
return
# Esegui la request e intercetta la response
response_body = []
async def send_wrapper(message):
if message["type"] == "http.response.body":
response_body.append(message.get("body", b""))
await send(message)
await self.app(scope, receive, send_wrapper)
# Salva in cache
if response_body:
body = b"".join(response_body)
await self.redis.setex(
cache_key,
self.DATASET_TTL["daily"], # Default: aggiornamento giornaliero
json.dumps({"body": body.decode(), "status_code": 200, "headers": {}})
)
def _generate_cache_key(self, url: str) -> str:
"""Genera cache key stabile dall'URL."""
return f"opendata:{hashlib.sha256(url.encode()).hexdigest()[:16]}"
Kvalita dat: Validace a profilování
Dataset, který je technicky dostupný, ale má nekvalitní data, není skutečně „otevřený“ v tomto smyslu užitečný termín. AgID definoval specifické rysy v tříletém plánu ICT 2024-2026 indikátory kvality pro datové sady PA inspirované kvalitativními rozměry normy ISO/IEC 25012:
| Dimenze kvality | Definice | Praktická metrika | Minimální prahová hodnota AgID |
|---|---|---|---|
| Úplnost | Žádné chybějící hodnoty | % NULL polí z celkového počtu | < 5 % NULL v povinných polích |
| Přesnost | Korespondence s realitou | Ověření proti autoritativním zdrojům | Záleží na doméně |
| Konzistence | Vnitřní konzistence datové sady | Referenční omezení, kontroly rozsahu | 0 % porušení omezení |
| Včasnost | Aktualizace datové sady podle deklarované frekvence | Počet dní od poslední aktualizace vs. frekvence | Aktualizováno během 2x uvedeného období |
| Dodržování | Soulad se standardy (DCAT-AP_IT) | Ověření metadat SHACL | Jsou přítomna 100% povinná metadata |
# Data Quality Profiler per dataset PA
import pandas as pd
import numpy as np
from dataclasses import dataclass, field
from typing import List, Dict, Any
@dataclass
class QualityReport:
dataset_id: str
total_rows: int
total_columns: int
quality_score: float # 0-100
issues: List[dict] = field(default_factory=list)
column_stats: Dict[str, Any] = field(default_factory=dict)
class DataQualityProfiler:
"""
Profiler di qualità per dataset open data PA.
Calcola metriche ISO/IEC 25012 e produce un report strutturato.
"""
REQUIRED_FIELDS = ["id", "timestamp", "value", "station_code"]
def profile(self, df: pd.DataFrame, dataset_id: str) -> QualityReport:
report = QualityReport(
dataset_id=dataset_id,
total_rows=len(df),
total_columns=len(df.columns),
quality_score=100.0
)
# 1. Completezza: campi obbligatori
for field_name in self.REQUIRED_FIELDS:
if field_name not in df.columns:
report.issues.append({
"severity": "critical",
"dimension": "completeness",
"field": field_name,
"message": f"Campo obbligatorio '{field_name}' mancante"
})
report.quality_score -= 20
else:
null_pct = df[field_name].isna().sum() / len(df) * 100
if null_pct > 5:
report.issues.append({
"severity": "warning",
"dimension": "completeness",
"field": field_name,
"message": f"{null_pct:.1f}% valori NULL nel campo '{field_name}'",
"null_count": int(df[field_name].isna().sum()),
"null_percentage": null_pct
})
report.quality_score -= min(10, null_pct)
# 2. Consistenza: duplicati
duplicate_count = df.duplicated().sum()
if duplicate_count > 0:
dup_pct = duplicate_count / len(df) * 100
report.issues.append({
"severity": "warning",
"dimension": "consistency",
"message": f"{duplicate_count} righe duplicate ({dup_pct:.1f}%)",
"duplicate_count": int(duplicate_count)
})
report.quality_score -= min(15, dup_pct * 2)
# 3. Statistiche per colonna
for col in df.columns:
col_stats = {
"dtype": str(df[col].dtype),
"null_count": int(df[col].isna().sum()),
"null_percentage": df[col].isna().sum() / len(df) * 100,
"unique_count": int(df[col].nunique()),
}
if df[col].dtype in [np.float64, np.int64]:
col_stats.update({
"min": float(df[col].min()),
"max": float(df[col].max()),
"mean": float(df[col].mean()),
"median": float(df[col].median()),
})
report.column_stats[col] = col_stats
report.quality_score = max(0.0, report.quality_score)
return report
Spotřeba otevřených dat: Robustní klienti
Na straně spotřeby mají veřejné datové soubory vlastnosti, které vyžadují zvláštní pozornost: mohou mít nepravidelné aktualizace, mohou být dočasně nedostupné, formáty se mohou mezi verzemi lišit stejného datového souboru a kvalita dat není zaručena. Robustní klient je musí zvládnout všechny tyto situace.
# Client robusto per consumare open data PA
import httpx
import asyncio
from typing import AsyncIterator
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
class RobustOpenDataClient:
"""
Client per consumo open data con retry, streaming e validazione.
"""
def __init__(self, timeout: int = 60):
self.timeout = timeout
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=60),
retry=retry_if_exception_type((httpx.HTTPError, httpx.TimeoutException))
)
async def fetch_dataset(self, url: str) -> dict:
"""Scarica un dataset con retry automatico in caso di errore."""
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.get(url, follow_redirects=True)
response.raise_for_status()
return response.json()
async def stream_large_csv(self, url: str) -> AsyncIterator[dict]:
"""
Streama CSV di grandi dimensioni senza caricare tutto in memoria.
Utile per dataset che possono essere di centinaia di MB.
"""
import csv
import io
async with httpx.AsyncClient(timeout=self.timeout) as client:
async with client.stream("GET", url) as response:
response.raise_for_status()
buffer = ""
headers = None
async for chunk in response.aiter_text(chunk_size=8192):
buffer += chunk
lines = buffer.split("\n")
# Mantieni l'ultima linea incompleta nel buffer
buffer = lines[-1]
complete_lines = lines[:-1]
if headers is None and complete_lines:
headers = list(csv.reader([complete_lines[0]]))[0]
complete_lines = complete_lines[1:]
if headers:
for line in complete_lines:
if line.strip():
row = list(csv.reader([line]))[0]
if len(row) == len(headers):
yield dict(zip(headers, row))
# Utilizzo: import dati ISTAT da API REST
async def import_istat_data():
client = RobustOpenDataClient()
# API ISTAT SDMX REST
istat_url = "https://esploradati.istat.it/SDMXWS/rest/data/IT1,DCSC_POPRES1_EV,1.0/A.IT.9.0?startPeriod=2020"
try:
data = await client.fetch_dataset(istat_url)
# Normalizza il formato SDMX
return normalize_sdmx_response(data)
except httpx.HTTPStatusError as e:
raise RuntimeError(f"ISTAT API error {e.response.status_code}: {e.response.text}")
PDND: Národní digitální datová platforma
La Národní digitální datová platforma (PDND), spravovaná společností PagoPA S.p.A., je infrastruktura národní systém interoperability, který umožňuje platebním agenturám sdílet data v bezpečném, kontrolovaném a dohledatelné. Na rozdíl od čistých otevřených dat (veřejná data přístupná komukoli) PDND také spravuje citlivé údaje, jejichž sdílení povolují interinstitucionální dohody.
Pro vývojáře znamená integrace PDND:
- Připojte se k PDND jako uživatel nebo poskytovatel prostřednictvím portálu interop.pagopa.it
- Publikovat API podle pokynů pro interoperabilitu AgID (povinné OpenAPI 3.1, e-služba s deskriptorem PDND)
- Ověřit prostřednictvím tokenů JWT podepsaných certifikáty X.509 pro každou žádost o data
- Respektujte poukázky na použití: digitální dohody mezi subjekty, které povolují přístup ke konkrétním rozhraním API
Závěry a další kroky
Kvalitní vládní otevřená data vyžadují více než jen zveřejnění souboru CSV na institucionálním webu: vyžaduje standardní metadatování (DCAT-AP_IT), dobře navržené REST API se stránkováním a ukládáním do mezipaměti, průběžné ověřování kvalita dat a integrace s národními infrastrukturami, jako jsou CKAN a PDND.
V dalším článku této série se budeme věnovat dostupnému uživatelskému rozhraní pro PA podle WCAG 2.1 AA: regulační požadavek stejně důležité, což spolu s otevřenými daty přispívá k tomu, aby veřejné digitální služby byly skutečně inkluzivní.
Užitečné zdroje
- data.gov.it - Národní portál otevřených dat Itálie
- Pokyny DCAT-AP_IT - Standardy metadat AgID
- ckanext-dcatapit - Rozšíření CKAN pro DCAT-AP_IT
- dokumentace PDND - Platforma interoperability
Související články v této sérii
- GovTech #00: Digitální veřejná infrastruktura - stavební kameny a architektura
- GovTech #04: GDPR-by-Design - architektonické vzory pro veřejné služby
- GovTech #06: Integrace vládního API - SPID, CIE a pagoPA
- GovTech #07: GovStack Building Block - moduly pro digitální vládu







