@aiaiai-pt/frankctl 0.5.1 → 0.7.0

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,68 @@
1
+ # Stage 01: 11 transport municipalities plus the Busway operator.
2
+ # Expected complete cardinality from the live source profile: 12.
3
+
4
+ apiVersion: frank.platform/v1
5
+ kind: Pipeline
6
+ metadata:
7
+ name: cira_transport_organization
8
+ spec:
9
+ name: cira_transport_organization
10
+ description: Canonical CIRA transport municipalities and operator organizations.
11
+ source_ids:
12
+ - cira-bdtransportes-rede
13
+ schedule_config:
14
+ type: manual
15
+ steps:
16
+ - name: organization_rows
17
+ kind: custom_sql
18
+ params:
19
+ sql: |-
20
+ SELECT concat('municipality:',cast(cast(fid AS bigint) AS varchar)) code,
21
+ municipio name,
22
+ CAST(NULL AS varchar) email,
23
+ CASE WHEN geometry_wkt IS NOT NULL THEN
24
+ concat(
25
+ '{"type":"MultiPolygon","coordinates":[',
26
+ json_format(json_extract(json_parse(to_geojson_geometry(ST_GeometryFromText(geometry_wkt))), '$.coordinates')),
27
+ ']}'
28
+ )
29
+ END boundary_geojson
30
+ FROM tenant_00000000_cira_bdtransportes_rede.municipios
31
+ UNION ALL
32
+ SELECT DISTINCT concat('operator:',cast(cast(id_operador_estrutura AS bigint) AS varchar)) code,
33
+ nome name,
34
+ CAST(NULL AS varchar) email,
35
+ CAST(NULL AS varchar) boundary_geojson
36
+ FROM tenant_00000000_cira_bdtransportes_rede.carreirastotal
37
+ depends_on: []
38
+ emits_to: backing_dataset
39
+ config:
40
+ output_layer: silver
41
+ ---
42
+ apiVersion: frank.platform/v1
43
+ kind: BackingDataset
44
+ metadata:
45
+ name: cira_transport_organization
46
+ spec:
47
+ entity_type_id: organization
48
+ entity_type_name: Organization
49
+ ontology_tenant_id: cira
50
+ pipeline: cira_transport_organization
51
+ sync_mode: manual
52
+ primary_key_column: code
53
+ title_key_column: name
54
+ property_mappings:
55
+ - column: code
56
+ property: code
57
+ type: varchar
58
+ is_primary_key: true
59
+ - column: name
60
+ property: name
61
+ type: varchar
62
+ - column: email
63
+ property: email
64
+ type: varchar
65
+ - column: boundary_geojson
66
+ property: boundary
67
+ type: varchar
68
+ sql_transform_id: json_parse
@@ -0,0 +1,73 @@
1
+ # Stage 02: CIRA transport freguesias. Run after stage 01 so every
2
+ # organization relationship resolves through organization.code.
3
+ # Expected complete cardinality from the live source profile: 74.
4
+
5
+ apiVersion: frank.platform/v1
6
+ kind: Pipeline
7
+ metadata:
8
+ name: cira_transport_freguesia
9
+ spec:
10
+ name: cira_transport_freguesia
11
+ description: Canonical CIRA transport freguesias with municipality ownership.
12
+ source_ids:
13
+ - cira-bdtransportes-rede
14
+ schedule_config:
15
+ type: manual
16
+ steps:
17
+ - name: freguesia_rows
18
+ kind: custom_sql
19
+ params:
20
+ sql: |-
21
+ SELECT dicofre code,
22
+ freguesia name,
23
+ concat('municipality:',cast(cast(idmunicipio AS bigint) AS varchar)) municipality_code,
24
+ concat('municipality:',cast(cast(idmunicipio AS bigint) AS varchar)) organization_code,
25
+ CASE WHEN geometry_wkt IS NOT NULL THEN
26
+ concat(
27
+ '{"type":"MultiPolygon","coordinates":[',
28
+ json_format(json_extract(json_parse(to_geojson_geometry(ST_GeometryFromText(geometry_wkt))), '$.coordinates')),
29
+ ']}'
30
+ )
31
+ END geometry_geojson
32
+ FROM tenant_00000000_cira_bdtransportes_rede.freguesias
33
+ WHERE dicofre IS NOT NULL
34
+ depends_on: []
35
+ emits_to: backing_dataset
36
+ config:
37
+ output_layer: silver
38
+ ---
39
+ apiVersion: frank.platform/v1
40
+ kind: BackingDataset
41
+ metadata:
42
+ name: cira_transport_freguesia
43
+ dependsOn:
44
+ - entityTypeId: organization
45
+ ontologyTenantId: cira
46
+ spec:
47
+ entity_type_id: freguesia
48
+ entity_type_name: Freguesia
49
+ ontology_tenant_id: cira
50
+ pipeline: cira_transport_freguesia
51
+ sync_mode: manual
52
+ primary_key_column: code
53
+ title_key_column: name
54
+ property_mappings:
55
+ - column: code
56
+ property: code
57
+ type: varchar
58
+ is_primary_key: true
59
+ - column: name
60
+ property: name
61
+ type: varchar
62
+ - column: municipality_code
63
+ property: municipality_code
64
+ type: varchar
65
+ - column: geometry_geojson
66
+ property: geometry
67
+ type: varchar
68
+ sql_transform_id: json_parse
69
+ - column: organization_code
70
+ property: organization
71
+ is_relationship: true
72
+ target_type: organization
73
+ target_key: code
@@ -0,0 +1,70 @@
1
+ # Stage 03: complete CIRA route projection. Run after stage 01 so every
2
+ # operator relationship resolves through organization.code.
3
+ # Expected complete cardinality from the live source profile: 8,964.
4
+
5
+ apiVersion: frank.platform/v1
6
+ kind: Pipeline
7
+ metadata:
8
+ name: cira_transport_routes
9
+ spec:
10
+ name: cira_transport_routes
11
+ description: Canonical CIRA public-transport routes and their operator.
12
+ source_ids:
13
+ - cira-bdtransportes-rede
14
+ schedule_config:
15
+ type: manual
16
+ steps:
17
+ - name: transit_route_rows
18
+ kind: custom_sql
19
+ params:
20
+ sql: |-
21
+ SELECT coda route_key,
22
+ designacao long_name,
23
+ 'bus' route_type,
24
+ nullif(regexp_replace(color_hex,'^#',''),'') color,
25
+ concat_ws(' · ',nullif(cod_car_operador,''),nullif(origem,''),nullif(destino,''),nullif(sentido,''),nullif(tipo,''),nullif(classificacao,'')) description,
26
+ concat('operator:',cast(cast(id_operador_estrutura AS bigint) AS varchar)) operator_code
27
+ FROM tenant_00000000_cira_bdtransportes_rede.carreirastotal
28
+ WHERE coda IS NOT NULL
29
+ depends_on: []
30
+ emits_to: backing_dataset
31
+ config:
32
+ output_layer: silver
33
+ ---
34
+ apiVersion: frank.platform/v1
35
+ kind: BackingDataset
36
+ metadata:
37
+ name: cira_transport_routes
38
+ dependsOn:
39
+ - entityTypeId: organization
40
+ ontologyTenantId: cira
41
+ spec:
42
+ entity_type_id: transit_route
43
+ entity_type_name: Transit Route
44
+ ontology_tenant_id: cira
45
+ pipeline: cira_transport_routes
46
+ sync_mode: manual
47
+ primary_key_column: route_key
48
+ title_key_column: long_name
49
+ property_mappings:
50
+ - column: route_key
51
+ property: short_name
52
+ type: varchar
53
+ is_primary_key: true
54
+ - column: long_name
55
+ property: long_name
56
+ type: varchar
57
+ - column: route_type
58
+ property: route_type
59
+ type: varchar
60
+ - column: color
61
+ property: color
62
+ type: varchar
63
+ - column: description
64
+ property: description
65
+ type: varchar
66
+ - column: operator_code
67
+ property: operator
68
+ is_relationship: true
69
+ target_type: organization
70
+ target_key: code
@@ -0,0 +1,57 @@
1
+ # Stage 04: complete CIRA public-stop projection.
2
+ # Expected complete cardinality from the live source profile: 2,411.
3
+
4
+ apiVersion: frank.platform/v1
5
+ kind: Pipeline
6
+ metadata:
7
+ name: cira_transport_stops
8
+ spec:
9
+ name: cira_transport_stops
10
+ description: Canonical CIRA public-transport stops with GeoJSON locations.
11
+ source_ids:
12
+ - cira-bdtransportes-rede
13
+ schedule_config:
14
+ type: manual
15
+ steps:
16
+ - name: transit_stop_rows
17
+ kind: custom_sql
18
+ params:
19
+ sql: |-
20
+ SELECT concat('stop:',cast(cast(id_paragem AS bigint) AS varchar)) stop_key,
21
+ designacao_paragem name,
22
+ nullif(trim(municipio),'') zone,
23
+ CASE WHEN geometry_wkt IS NOT NULL THEN to_geojson_geometry(ST_GeometryFromText(geometry_wkt)) END location_geojson
24
+ FROM tenant_00000000_cira_bdtransportes_rede.paragenspublicogeral
25
+ WHERE id_paragem IS NOT NULL
26
+ depends_on: []
27
+ emits_to: backing_dataset
28
+ config:
29
+ output_layer: silver
30
+ ---
31
+ apiVersion: frank.platform/v1
32
+ kind: BackingDataset
33
+ metadata:
34
+ name: cira_transport_stops
35
+ spec:
36
+ entity_type_id: transit_stop
37
+ entity_type_name: Transit Stop
38
+ ontology_tenant_id: cira
39
+ pipeline: cira_transport_stops
40
+ sync_mode: manual
41
+ primary_key_column: stop_key
42
+ title_key_column: name
43
+ property_mappings:
44
+ - column: stop_key
45
+ property: code
46
+ type: varchar
47
+ is_primary_key: true
48
+ - column: name
49
+ property: name
50
+ type: varchar
51
+ - column: zone
52
+ property: zone
53
+ type: varchar
54
+ - column: location_geojson
55
+ property: location
56
+ type: varchar
57
+ sql_transform_id: json_parse
@@ -0,0 +1,83 @@
1
+ # Stage 05: station natural keys from the raw Kunak and VDL tables.
2
+ # This deliberately avoids the existing duplicate-name Silver projection.
3
+ # Expected complete cardinality from the live source profile: 13 unique names.
4
+
5
+ apiVersion: frank.platform/v1
6
+ kind: Pipeline
7
+ metadata:
8
+ name: cira_air_quality_stations
9
+ spec:
10
+ name: cira_air_quality_stations
11
+ description: Canonical unique CIRA air-quality stations from Kunak and VDL.
12
+ source_ids:
13
+ - Kunak Cloud - Ubiwhere
14
+ - VDL - AirQuality - Monitar
15
+ schedule_config:
16
+ type: manual
17
+ steps:
18
+ - name: air_quality_station_rows
19
+ kind: custom_sql
20
+ params:
21
+ sql: |-
22
+ WITH kunak AS (
23
+ SELECT serial_number,
24
+ max(tag) label,
25
+ max(try_cast(coalesce(nullif(trim(location__latitude),''),try(json_extract_scalar(location,'$.latitude'))) AS double)) latitude,
26
+ max(try_cast(coalesce(nullif(trim(location__longitude),''),try(json_extract_scalar(location,'$.longitude'))) AS double)) longitude
27
+ FROM tenant_00000000_kunak_cloud_ubiwhere.kunak_stations
28
+ WHERE serial_number IS NOT NULL
29
+ GROUP BY serial_number
30
+ ), vdl AS (
31
+ SELECT system
32
+ FROM tenant_00000000_vdl_airquality_monitar.sensors
33
+ WHERE system IS NOT NULL
34
+ GROUP BY system
35
+ )
36
+ SELECT concat('kunak:',serial_number) station_key,
37
+ label alternate_name,
38
+ 'Kunak' data_provider,
39
+ 'kunak_cloud_ubiwhere' source,
40
+ CASE WHEN latitude IS NOT NULL AND longitude IS NOT NULL THEN to_geojson_geometry(ST_GeometryFromText(format('POINT (%s %s)',longitude,latitude))) END location_geojson
41
+ FROM kunak
42
+ UNION ALL
43
+ SELECT concat('vdl:',system),
44
+ system,
45
+ 'Monitar',
46
+ 'vdl_airquality_monitar',
47
+ CAST(NULL AS varchar)
48
+ FROM vdl
49
+ depends_on: []
50
+ emits_to: backing_dataset
51
+ config:
52
+ output_layer: silver
53
+ ---
54
+ apiVersion: frank.platform/v1
55
+ kind: BackingDataset
56
+ metadata:
57
+ name: cira_air_quality_stations
58
+ spec:
59
+ entity_type_id: air_quality_station
60
+ entity_type_name: Air Quality Station
61
+ ontology_tenant_id: cira
62
+ pipeline: cira_air_quality_stations
63
+ sync_mode: manual
64
+ primary_key_column: station_key
65
+ title_key_column: station_key
66
+ property_mappings:
67
+ - column: station_key
68
+ property: name
69
+ type: varchar
70
+ is_primary_key: true
71
+ - column: alternate_name
72
+ property: alternate_name
73
+ type: varchar
74
+ - column: data_provider
75
+ property: data_provider
76
+ type: varchar
77
+ - column: source
78
+ property: source
79
+ type: varchar
80
+ - column: location_geojson
81
+ property: location
82
+ type: varchar
83
+ sql_transform_id: json_parse
@@ -0,0 +1,99 @@
1
+ # Stage 06: the complete publishable Kunak observation subset for the five
2
+ # source pollutant tags supported by the target schema. Run after stage 05 so
3
+ # ref_station resolves.
4
+ #
5
+ # Live raw profile (2026-03-23 through 2026-06-22): 3,169,139 raw measurement rows
6
+ # across 115,665 raw timestamps. The five-tag predicate retains 542,406
7
+ # pollutant-tag rows and 26,216 canonical observation timestamps. Keeping this
8
+ # predicate prevents publishing 89,449 timestamp entities whose every mapped
9
+ # pollutant would be null. Pressure, temperature, humidity, and battery are not
10
+ # target fields; other target pollutant fields are absent from this Kunak tag set.
11
+
12
+ apiVersion: frank.platform/v1
13
+ kind: Pipeline
14
+ metadata:
15
+ name: cira_air_quality_observed
16
+ spec:
17
+ name: cira_air_quality_observed
18
+ description: Complete Kunak air-quality observations linked to canonical stations.
19
+ source_ids:
20
+ - Kunak Cloud - Ubiwhere
21
+ schedule_config:
22
+ type: manual
23
+ steps:
24
+ - name: air_quality_observed_rows
25
+ kind: custom_sql
26
+ params:
27
+ sql: |-
28
+ SELECT concat('kunak:0425100035:',cast(ts AS varchar)) observation_key,
29
+ from_unixtime(cast(ts AS double)/1000.0) date_observed,
30
+ 'Kunak' data_provider,
31
+ 'kunak_cloud_ubiwhere' source,
32
+ 'kunak:0425100035' station_name,
33
+ 'kunak:0425100035' ref_station_key,
34
+ max(CASE WHEN sensor_tag='PM1' THEN try_cast(value AS double) END) pm1,
35
+ max(CASE WHEN sensor_tag='PM2.5' THEN try_cast(value AS double) END) pm25,
36
+ max(CASE WHEN sensor_tag='PM10' THEN try_cast(value AS double) END) pm10,
37
+ max(CASE WHEN sensor_tag='NO2 GCc' THEN try_cast(value AS double) END) no2,
38
+ max(CASE WHEN sensor_tag='CO GCc' THEN try_cast(value AS double) END) co
39
+ FROM tenant_00000000_kunak_cloud_ubiwhere.device_0425100035
40
+ WHERE ts IS NOT NULL
41
+ AND sensor_tag IN ('PM1','PM2.5','PM10','NO2 GCc','CO GCc')
42
+ GROUP BY ts
43
+ depends_on: []
44
+ emits_to: backing_dataset
45
+ config:
46
+ output_layer: silver
47
+ ---
48
+ apiVersion: frank.platform/v1
49
+ kind: BackingDataset
50
+ metadata:
51
+ name: cira_air_quality_observed
52
+ dependsOn:
53
+ - entityTypeId: air_quality_station
54
+ ontologyTenantId: cira
55
+ spec:
56
+ entity_type_id: air_quality_observed
57
+ entity_type_name: Air Quality Observed
58
+ ontology_tenant_id: cira
59
+ pipeline: cira_air_quality_observed
60
+ sync_mode: manual
61
+ primary_key_column: observation_key
62
+ title_key_column: observation_key
63
+ property_mappings:
64
+ - column: observation_key
65
+ property: name
66
+ type: varchar
67
+ is_primary_key: true
68
+ - column: date_observed
69
+ property: date_observed
70
+ type: timestamp with time zone
71
+ - column: data_provider
72
+ property: data_provider
73
+ type: varchar
74
+ - column: source
75
+ property: source
76
+ type: varchar
77
+ - column: station_name
78
+ property: station_name
79
+ type: varchar
80
+ - column: ref_station_key
81
+ property: ref_station
82
+ is_relationship: true
83
+ target_type: air_quality_station
84
+ target_key: name
85
+ - column: pm1
86
+ property: pm1
87
+ type: double
88
+ - column: pm25
89
+ property: pm25
90
+ type: double
91
+ - column: pm10
92
+ property: pm10
93
+ type: double
94
+ - column: no2
95
+ property: no2
96
+ type: double
97
+ - column: co
98
+ property: co
99
+ type: double
@@ -0,0 +1,106 @@
1
+ # Stage 07: complete CIRA trips after the repaired WFS iterator publishes the
2
+ # authoritative 237,808-row schedule snapshot. The point-in-time source profile
3
+ # has 8,964 unique, non-null trip IDs; the count is evidence, not a row limit.
4
+
5
+ apiVersion: frank.platform/v1
6
+ kind: Source
7
+ metadata:
8
+ name: cira-bdtransportes-horarios
9
+ spec:
10
+ name: cira-bdtransportes-horarios
11
+ pattern_id: geoserver_wfs
12
+ source_config:
13
+ base_url: https://transportes.regiaodeaveiro.pt/geoserver
14
+ workspace: cimo
15
+ resources:
16
+ - ParagensTotalSchedule
17
+ wfs_version: 2.0.0
18
+ srs_name: EPSG:4326
19
+ page_size: 10000
20
+ # Match one iterator activity to one WFS page. The 5k cluster default
21
+ # re-fetches every 10k page for its second half, which is correct but adds a
22
+ # slow duplicate sorted-offset request on this 237,808-row source.
23
+ max_items_per_chunk: 10000
24
+ schedule_type: manual
25
+ streams:
26
+ - name: ParagensTotalSchedule
27
+ sync_mode: full_refresh
28
+ write_disposition: replace
29
+ primary_key_path:
30
+ - trip_id
31
+ - Ordem
32
+ - IdParagem
33
+ is_enabled: true
34
+ ---
35
+ apiVersion: frank.platform/v1
36
+ kind: Pipeline
37
+ metadata:
38
+ name: cira_transport_trips
39
+ spec:
40
+ name: cira_transport_trips
41
+ description: Complete CIRA transit trips with deterministic final-stop headsigns.
42
+ source_ids:
43
+ - cira-bdtransportes-horarios
44
+ schedule_config:
45
+ type: manual
46
+ steps:
47
+ - name: transit_trip_rows
48
+ kind: custom_sql
49
+ params:
50
+ sql: |-
51
+ WITH final_stop AS (
52
+ SELECT trip_id service_id,
53
+ try_cast(sentidoparagem AS integer) direction,
54
+ nullif(trim(paragem),'') headsign,
55
+ coda route_key,
56
+ row_number() OVER (
57
+ PARTITION BY trip_id
58
+ ORDER BY try_cast(ordem AS bigint) DESC NULLS LAST,
59
+ try_cast(id_paragem AS bigint) DESC NULLS LAST,
60
+ _feature_id DESC NULLS LAST
61
+ ) stop_rank
62
+ FROM tenant_00000000_cira_bdtransportes_horarios.paragenstotalschedule
63
+ WHERE trip_id IS NOT NULL
64
+ )
65
+ SELECT service_id,
66
+ direction,
67
+ headsign,
68
+ route_key
69
+ FROM final_stop
70
+ WHERE stop_rank=1
71
+ depends_on: []
72
+ emits_to: backing_dataset
73
+ config:
74
+ output_layer: silver
75
+ ---
76
+ apiVersion: frank.platform/v1
77
+ kind: BackingDataset
78
+ metadata:
79
+ name: cira_transport_trips
80
+ dependsOn:
81
+ - entityTypeId: transit_route
82
+ ontologyTenantId: cira
83
+ spec:
84
+ entity_type_id: transit_trip
85
+ entity_type_name: Transit Trip
86
+ ontology_tenant_id: cira
87
+ pipeline: cira_transport_trips
88
+ sync_mode: manual
89
+ primary_key_column: service_id
90
+ title_key_column: service_id
91
+ property_mappings:
92
+ - column: service_id
93
+ property: service_id
94
+ type: varchar
95
+ is_primary_key: true
96
+ - column: direction
97
+ property: direction
98
+ type: integer
99
+ - column: headsign
100
+ property: headsign
101
+ type: varchar
102
+ - column: route_key
103
+ property: route
104
+ is_relationship: true
105
+ target_type: transit_route
106
+ target_key: short_name
@@ -0,0 +1,81 @@
1
+ # Stage 08: the current CIRA AVL snapshot. The upstream point-in-time profile
2
+ # at 2026-07-21 20:11 UTC contained 405 rows with 405 unique, non-null partition
3
+ # IDs (up from 403 minutes earlier). Every full refresh replaces that evidence
4
+ # point with the then-current set.
5
+
6
+ apiVersion: frank.platform/v1
7
+ kind: Source
8
+ metadata:
9
+ name: cira-bdtransportes-avl
10
+ spec:
11
+ name: cira-bdtransportes-avl
12
+ pattern_id: geoserver_wfs
13
+ source_config:
14
+ base_url: https://transportes.regiaodeaveiro.pt/geoserver
15
+ workspace: cimo
16
+ resources:
17
+ - carreirasae_ultimaposicao
18
+ wfs_version: 2.0.0
19
+ srs_name: EPSG:4326
20
+ page_size: 5000
21
+ primary_keys:
22
+ - partition_id
23
+ schedule_type: manual
24
+ streams:
25
+ - name: carreirasae_ultimaposicao
26
+ sync_mode: full_refresh
27
+ write_disposition: replace
28
+ primary_key_path:
29
+ - partition_id
30
+ is_enabled: true
31
+ ---
32
+ apiVersion: frank.platform/v1
33
+ kind: Pipeline
34
+ metadata:
35
+ name: cira_transport_vehicle_positions
36
+ spec:
37
+ name: cira_transport_vehicle_positions
38
+ description: Current CIRA vehicle positions without an unproven trip relationship.
39
+ source_ids:
40
+ - cira-bdtransportes-avl
41
+ schedule_config:
42
+ type: manual
43
+ steps:
44
+ - name: vehicle_position_rows
45
+ kind: custom_sql
46
+ params:
47
+ sql: |-
48
+ SELECT partition_id label,
49
+ CASE WHEN geometry_wkt IS NOT NULL THEN to_geojson_geometry(ST_GeometryFromText(geometry_wkt)) END location_geojson,
50
+ try_cast(data AS timestamp) recorded_at
51
+ FROM tenant_00000000_cira_bdtransportes_avl.carreirasae_ultimaposicao
52
+ WHERE partition_id IS NOT NULL
53
+ depends_on: []
54
+ emits_to: backing_dataset
55
+ config:
56
+ output_layer: silver
57
+ ---
58
+ apiVersion: frank.platform/v1
59
+ kind: BackingDataset
60
+ metadata:
61
+ name: cira_transport_vehicle_positions
62
+ spec:
63
+ entity_type_id: vehicle_position
64
+ entity_type_name: Vehicle Position
65
+ ontology_tenant_id: cira
66
+ pipeline: cira_transport_vehicle_positions
67
+ sync_mode: manual
68
+ primary_key_column: label
69
+ title_key_column: label
70
+ property_mappings:
71
+ - column: label
72
+ property: label
73
+ type: varchar
74
+ is_primary_key: true
75
+ - column: location_geojson
76
+ property: location
77
+ type: varchar
78
+ sql_transform_id: json_parse
79
+ - column: recorded_at
80
+ property: recorded_at
81
+ type: timestamp