Skip to content

Commit b350e63

Browse files
committed
Wire resource_status in the consolidation
1 parent da91964 commit b350e63

3 files changed

Lines changed: 144 additions & 4 deletions

File tree

apps/transport/lib/irve/consolidation.ex

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Transport.IRVE.Consolidation do
22
@moduledoc """
33
This module:
4-
- takes the list of relevant IRVE resources from data.gouv.fr,
4+
- takes the list of relevant IRVE datagouv_resources from data.gouv.fr,
55
- downloads each resource file,
66
- validates it,
77
- and if valid writes the PDCs and file to the database.
@@ -29,8 +29,13 @@ defmodule Transport.IRVE.Consolidation do
2929
destination = Keyword.get(opts, :destination, :send_to_s3)
3030
debug = Keyword.get(opts, :debug, false)
3131

32-
report_rows =
33-
resource_list()
32+
datagouv_resources = resource_list()
33+
datagouv_resource_ids = MapSet.new(datagouv_resources, & &1.resource_id)
34+
# Snapshot before processing mutates the DB, so `resource_status` reflects the pre-run state.
35+
db_resource_ids = DB.IRVEValidFile.existing_datagouv_resource_ids()
36+
37+
processed_rows =
38+
datagouv_resources
3439
|> maybe_limit(opts[:limit])
3540
|> Task.async_stream(
3641
&process_or_rescue/1,
@@ -45,9 +50,12 @@ defmodule Transport.IRVE.Consolidation do
4550
# This is intentional, we want to be aware of such timeouts.
4651
|> Stream.map(fn {:ok, result} -> result end)
4752
|> Stream.map(&Transport.IRVE.ReportItem.from_result/1)
53+
|> Stream.map(&Transport.IRVE.ReportItem.put_resource_status(&1, db_resource_ids))
4854
|> maybe_log_items(debug)
4955
|> Enum.into([])
5056

57+
report_rows = processed_rows ++ orphan_report_rows(db_resource_ids, datagouv_resource_ids)
58+
5159
report = generate_report(report_rows, destination: destination)
5260

5361
consolidated_df =
@@ -85,6 +93,17 @@ defmodule Transport.IRVE.Consolidation do
8593
|> Enum.sort_by(fn r -> [r.dataset_id, r.resource_id] end)
8694
end
8795

96+
# Resources still in the DB but no longer listed on data.gouv.fr, appended as a block after the
97+
# processed rows (which are not processed this run, so they carry no consolidation outcome).
98+
def orphan_report_rows(db_resource_ids, datagouv_resource_ids) do
99+
db_resource_ids
100+
|> MapSet.difference(datagouv_resource_ids)
101+
|> MapSet.to_list()
102+
|> DB.IRVEValidFile.orphan_files()
103+
|> Enum.sort_by(&{&1.datagouv_dataset_id, &1.datagouv_resource_id})
104+
|> Enum.map(&Transport.IRVE.ReportItem.from_orphan_file/1)
105+
end
106+
88107
# safety wrapper that we can use inside `Task.async_stream`
89108
def process_or_rescue(resource) do
90109
process_resource(resource)
@@ -149,6 +168,7 @@ defmodule Transport.IRVE.Consolidation do
149168
|> Explorer.DataFrame.select([
150169
"dataset_id",
151170
"resource_id",
171+
"resource_status",
152172
"consolidation_status",
153173
"error_type",
154174
"estimated_pdc_count",

apps/transport/lib/irve/report_item.ex

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ defmodule Transport.IRVE.ReportItem do
1212
:dataset_title,
1313
:datagouv_organization_or_owner,
1414
:datagouv_last_modified,
15+
:resource_status,
1516
:consolidation_status,
1617
:error_message,
1718
:error_type,
@@ -35,12 +36,55 @@ defmodule Transport.IRVE.ReportItem do
3536
new(resource, status, nil, nil)
3637
end
3738

39+
# Presence relationship between data.gouv.fr and our database, from a pre-run snapshot of the
40+
# resource ids in the database. Kept separate from the consolidation outcome.
41+
def put_resource_status(%__MODULE__{resource_id: resource_id} = report_row, db_resource_ids) do
42+
resource_status =
43+
if MapSet.member?(db_resource_ids, resource_id),
44+
do: :on_datagouv_and_in_db,
45+
else: :on_datagouv_not_in_db
46+
47+
%{report_row | resource_status: resource_status}
48+
end
49+
50+
# A resource still in the database but no longer listed on data.gouv.fr: no download/processing
51+
# happened this run, so `consolidation_status` and the download-related fields stay nil.
52+
def from_orphan_file(%{
53+
datagouv_dataset_id: dataset_id,
54+
datagouv_resource_id: resource_id,
55+
dataset_title: dataset_title,
56+
datagouv_organization_or_owner: datagouv_organization_or_owner,
57+
datagouv_last_modified: datagouv_last_modified,
58+
pdc_count: pdc_count
59+
}) do
60+
%__MODULE__{
61+
dataset_id: dataset_id,
62+
resource_id: resource_id,
63+
url: nil,
64+
dataset_title: dataset_title,
65+
datagouv_organization_or_owner: datagouv_organization_or_owner,
66+
datagouv_last_modified: datagouv_last_modified,
67+
resource_status: :in_db_deleted_from_datagouv,
68+
consolidation_status: nil,
69+
estimated_pdc_count: pdc_count,
70+
error_message: nil,
71+
error_type: nil,
72+
file_extension: nil
73+
}
74+
end
75+
3876
def to_map(%__MODULE__{} = report_row) do
3977
report_row
4078
|> Map.from_struct()
41-
|> Map.update!(:consolidation_status, &to_string/1)
79+
|> Map.update!(:consolidation_status, &stringify/1)
80+
|> Map.update!(:resource_status, &stringify/1)
81+
|> Map.update!(:datagouv_last_modified, &stringify/1)
4282
end
4383

84+
# `nil` (empty CSV cell) for orphan rows; keeps mixed atom/DateTime/string columns uniform.
85+
defp stringify(nil), do: nil
86+
defp stringify(value), do: to_string(value)
87+
4488
defp new(resource, status, error_message, error_type) do
4589
%__MODULE__{
4690
dataset_id: resource.dataset_id,

apps/transport/test/transport/irve/consolidation_test.exs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ defmodule Transport.IRVE.ConsolidationTest do
3333
%{
3434
"dataset_id" => "another-dataset-id",
3535
"resource_id" => "another-resource-id",
36+
"resource_status" => "on_datagouv_not_in_db",
3637
"consolidation_status" => "file_level_errors",
3738
"error_type" => nil,
3839
"estimated_pdc_count" => "1",
@@ -50,6 +51,7 @@ defmodule Transport.IRVE.ConsolidationTest do
5051
%{
5152
"dataset_id" => "individual-published-dataset-id",
5253
"resource_id" => "individual-published-resource-id",
54+
"resource_status" => "on_datagouv_not_in_db",
5355
"consolidation_status" => "producer_not_an_organization",
5456
"error_type" => nil,
5557
"estimated_pdc_count" => "1",
@@ -63,6 +65,7 @@ defmodule Transport.IRVE.ConsolidationTest do
6365
%{
6466
"dataset_id" => "the-dataset-id",
6567
"resource_id" => "the-resource-id",
68+
"resource_status" => "on_datagouv_not_in_db",
6669
"consolidation_status" => "import_successful",
6770
"error_type" => nil,
6871
"estimated_pdc_count" => "1",
@@ -79,6 +82,7 @@ defmodule Transport.IRVE.ConsolidationTest do
7982
|> Explorer.DataFrame.select([
8083
"dataset_id",
8184
"resource_id",
85+
"resource_status",
8286
"consolidation_status",
8387
"error_type",
8488
"estimated_pdc_count",
@@ -222,6 +226,78 @@ defmodule Transport.IRVE.ConsolidationTest do
222226
refute File.exists?(System.tmp_dir!() |> Path.join("irve-resource-the-resource-id.dat"))
223227
refute File.exists?(System.tmp_dir!() |> Path.join("irve-resource-another-resource-id.dat"))
224228
end
229+
230+
test "reports the presence delta between data.gouv.fr and the database" do
231+
mock_datagouv_resources()
232+
mock_resource_downloads()
233+
234+
on_exit(fn ->
235+
~w(
236+
consolidation_transport_irve_statique_rapport.csv
237+
consolidation_transport_avec_doublons_irve_statique.csv
238+
consolidation_transport_irve_statique.csv
239+
)
240+
|> Enum.each(&File.rm/1)
241+
end)
242+
243+
# Still in the DB but no longer listed on data.gouv.fr, with 2 stored PDCs.
244+
seed_file_with_pdcs("deleted-dataset-id", "deleted-resource-id", 2)
245+
246+
# Already in the DB (older version) and still listed on data.gouv.fr.
247+
DB.Repo.insert!(%DB.IRVEValidFile{
248+
datagouv_dataset_id: "the-dataset-id",
249+
datagouv_resource_id: "the-resource-id",
250+
checksum: "old-checksum"
251+
})
252+
253+
{:ok, report_df} = Transport.IRVE.Consolidation.process(destination: :local_disk)
254+
255+
rows_by_id =
256+
report_df
257+
|> Explorer.DataFrame.to_rows()
258+
|> Map.new(fn row -> {row["resource_id"], row} end)
259+
260+
# Had a version in the DB, imported a new one this run.
261+
assert rows_by_id["the-resource-id"]["resource_status"] == "on_datagouv_and_in_db"
262+
assert rows_by_id["the-resource-id"]["consolidation_status"] == "import_successful"
263+
264+
# Brand-new resources (nothing in the DB before this run).
265+
assert rows_by_id["another-resource-id"]["resource_status"] == "on_datagouv_not_in_db"
266+
assert rows_by_id["individual-published-resource-id"]["resource_status"] == "on_datagouv_not_in_db"
267+
268+
# Invariant: cannot be up-to-date without being in the DB.
269+
refute Enum.any?(rows_by_id, fn {_id, row} ->
270+
row["resource_status"] == "on_datagouv_not_in_db" and
271+
row["consolidation_status"] == "already_up_to_date"
272+
end)
273+
274+
orphan = rows_by_id["deleted-resource-id"]
275+
assert orphan["resource_status"] == "in_db_deleted_from_datagouv"
276+
assert is_nil(orphan["consolidation_status"])
277+
assert orphan["estimated_pdc_count"] == 2
278+
assert orphan["dataset_id"] == "deleted-dataset-id"
279+
end
280+
end
281+
282+
defp seed_file_with_pdcs(dataset_id, resource_id, pdc_count) do
283+
rows =
284+
for i <- 1..pdc_count do
285+
DB.Factory.IRVE.generate_row(%{"id_pdc_itinerance" => "FRPAN99E0000000#{i}"})
286+
end
287+
288+
path = System.tmp_dir!() |> Path.join("irve_seed_#{Ecto.UUID.generate()}.csv")
289+
File.write!(path, DB.Factory.IRVE.to_csv_body(rows))
290+
291+
Transport.IRVE.DatabaseImporter.write_to_db(
292+
path,
293+
dataset_id,
294+
resource_id,
295+
"#{dataset_id}-title",
296+
"#{dataset_id}-org",
297+
"2024-01-01T10:00:00+00:00"
298+
)
299+
300+
File.rm(path)
225301
end
226302

227303
defp mock_datagouv_resources do

0 commit comments

Comments
 (0)