technologydata 0.1.0__py3-none-any.whl

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,691 @@
1
+ # SPDX-FileCopyrightText: technologydata contributors
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ """
6
+ Data parser for the DEA energy storage data set.
7
+
8
+ How to run:
9
+ From the repository root, execute:
10
+ python src/technologydata/package_data/dea_energy_storage/dea_energy_storage.py
11
+
12
+ Configuration options (command-line arguments):
13
+ --num_digits <int> Number of significant digits to round the values. Default: 4
14
+ --store_source Store the source object on the Wayback Machine. Default: False
15
+ --filter_params Filter the parameters stored to technologies.json. Default: False
16
+
17
+ Example:
18
+ python src/technologydata/package_data/dea_energy_storage/dea_energy_storage.py --num_digits 3 --store_source --filter_params
19
+
20
+ """
21
+
22
+ import argparse
23
+ import logging
24
+ import pathlib
25
+ import re
26
+ import typing
27
+
28
+ import pandas as pd
29
+ import pydantic
30
+
31
+ from technologydata import (
32
+ Commons,
33
+ Parameter,
34
+ Source,
35
+ SourceCollection,
36
+ Technology,
37
+ TechnologyCollection,
38
+ )
39
+
40
+ path_cwd = pathlib.Path.cwd()
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+
45
+ def drop_invalid_rows(dataframe: pd.DataFrame) -> pd.DataFrame:
46
+ """
47
+ Clean and filter a DataFrame by removing rows with invalid or incomplete data.
48
+
49
+ This function performs multiple validation checks to ensure data quality:
50
+ - Removes rows with None or NaN values in critical columns
51
+ - Removes rows with empty or whitespace-only strings
52
+ - Filters rows based on specific data integrity criteria
53
+ - Discards rows where 'val' column contains comparator symbols or non-numeric values
54
+
55
+ Parameters
56
+ ----------
57
+ dataframe : pd.DataFrame
58
+ The input DataFrame to be cleaned and validated.
59
+
60
+ Returns
61
+ -------
62
+ pd.DataFrame
63
+ A new DataFrame with invalid rows removed, maintaining data integrity.
64
+
65
+ Notes
66
+ -----
67
+ Validation criteria include:
68
+ - Non-empty 'Technology', 'par', and 'val' columns
69
+ - 'year' column containing a valid 4-digit year
70
+ - 'val' column containing only numeric values (no comparator symbols)
71
+
72
+ """
73
+ # Create a copy to avoid modifying the original DataFrame
74
+ df_cleaned = dataframe.copy()
75
+
76
+ # Validate column existence
77
+ required_columns = ["Technology", "par", "val", "year"]
78
+ missing_columns = [col for col in required_columns if col not in df_cleaned.columns]
79
+ if missing_columns:
80
+ raise ValueError(f"Missing required columns: {missing_columns}")
81
+
82
+ # Remove rows with None or NaN values in critical columns
83
+ df_cleaned = df_cleaned.dropna(subset=required_columns)
84
+
85
+ # Remove rows with empty or whitespace-only strings
86
+ for column in required_columns:
87
+ df_cleaned = df_cleaned[df_cleaned[column].astype(str).str.strip() != ""]
88
+
89
+ # Filter rows with valid year (4 consecutive digits)
90
+ df_cleaned = df_cleaned[
91
+ df_cleaned["year"].astype(str).str.contains(r"\d{4}", regex=True)
92
+ ]
93
+
94
+ # Remove rows with comparator symbols or without digits in 'val' column
95
+ df_cleaned = df_cleaned[
96
+ (~df_cleaned["val"].astype(str).str.contains(r"[<>≤≥]", regex=True))
97
+ & (df_cleaned["val"].astype(str).str.contains(r"\d", regex=True))
98
+ ]
99
+
100
+ return df_cleaned
101
+
102
+
103
+ @pydantic.validate_call
104
+ def clean_parameter_string(text_string: str) -> str:
105
+ """
106
+ Remove any string between [] or [), any leading hyphen or double quotes from the input string. Lower-case all.
107
+
108
+ Parameters
109
+ ----------
110
+ text_string : str
111
+ input string to be cleaned.
112
+
113
+ Returns
114
+ -------
115
+ str
116
+ cleaned string with [] and [) and leading hyphen or double quotes removed.
117
+
118
+ Examples
119
+ --------
120
+ >>> clean_parameter_string("- Charge efficiency [%]")
121
+ charge efficiency
122
+ >>> clean_parameter_string("Energy storage capacity for one unit [MWh)")
123
+ energy storage capacity for one unit
124
+
125
+ """
126
+ # Remove leading hyphen
127
+ text_string = text_string.lstrip("-")
128
+
129
+ # Remove content inside square brackets including the brackets themselves
130
+ result = re.sub(r"\[.*?\]", "", text_string)
131
+
132
+ # Remove content inside square bracket and parenthesis including the brackets/parenthesis themselves
133
+ result = re.sub(r"\[.*?\)", "", result)
134
+
135
+ # Remove extra spaces resulting from the removal and set all to lower case
136
+ result = re.sub(r"\s+", " ", result).strip().casefold()
137
+
138
+ return result
139
+
140
+
141
+ @pydantic.validate_call
142
+ def clean_technology_string(tech_str: str) -> str:
143
+ """
144
+ Clean a technology string by removing numeric patterns and standardizing case.
145
+
146
+ This function pre-processes technology-related strings by:
147
+ - Removing three-digit numeric patterns (with optional letter)
148
+ - Stripping leading and trailing whitespace
149
+ - Converting to lowercase for case-insensitive comparison
150
+
151
+ Parameters
152
+ ----------
153
+ tech_str : str
154
+ Input technology string to be cleaned.
155
+
156
+ Returns
157
+ -------
158
+ str
159
+ Cleaned technology string with:
160
+ - Numeric patterns (like '123' or '456a') removed
161
+ - Whitespace stripped
162
+ - Converted to lowercase
163
+
164
+ Raises
165
+ ------
166
+ Exception
167
+ If string conversion or processing fails, logs the error and returns the original input.
168
+
169
+ Examples
170
+ --------
171
+ >>> clean_technology_string("143a Rock-based Carnot battery")
172
+ rock-based carnot battery
173
+ >>> clean_technology_string("Pit Thermal Energy Storage [PTES]")
174
+ pit thermal energy storage [ptes]
175
+
176
+ """
177
+ try:
178
+ # Remove three-digit patterns or three digits followed by a letter
179
+ return re.sub(r"^(\d{3}[a-zA-Z]?)", "", tech_str.strip()).strip().casefold()
180
+ except Exception as e:
181
+ logger.error(f"Error cleaning technology '{tech_str}': {e}")
182
+ return tech_str
183
+
184
+
185
+ @pydantic.validate_call
186
+ def format_val_number(input_value: str, num_decimals: int) -> float | None | typing.Any:
187
+ """
188
+ Parse various number formats into a float value.
189
+
190
+ Parameters
191
+ ----------
192
+ input_value : str
193
+ The input number in different formats, such as:
194
+ - Scientific notation with "x10^": e.g., "2.84x10^23"
195
+ - Numbers with commas as decimal separators: e.g., "1,1"
196
+ num_decimals : int
197
+ Number of decimals
198
+
199
+ Returns
200
+ -------
201
+ float
202
+ The parsed numerical value as a float.
203
+
204
+ Raises
205
+ ------
206
+ ValueError
207
+ If the input cannot be parsed into a float.
208
+
209
+ Examples
210
+ --------
211
+ >>> format_val_number("1,1")
212
+ 1.1
213
+ >>> format_val_numer("2.84×10-27")
214
+ 2.84e-27
215
+
216
+ """
217
+ s = str(input_value).strip()
218
+
219
+ # Handle scientific notation like "2.84x10^23"
220
+ match = re.match(r"([+-]?\d*\.?\d+)×10([+-]?\d+)", s)
221
+ if match:
222
+ base, exponent = match.groups()
223
+ return round(float(base), num_decimals) * (10 ** int(exponent))
224
+
225
+ # Replace comma with dot for decimal numbers
226
+ s = s.replace(",", ".")
227
+ try:
228
+ return round(float(s), num_decimals)
229
+ except ValueError:
230
+ raise ValueError(f"Cannot parse number from input: {input_value}")
231
+
232
+
233
+ @pydantic.validate_call
234
+ def extract_year(year_str: str) -> int | None:
235
+ """
236
+ Extract the first year (integer) from a given input.
237
+
238
+ Parameters
239
+ ----------
240
+ year_str : str
241
+ Input value containing a potential year.
242
+
243
+ Returns
244
+ -------
245
+ int, None
246
+ Extracted first year.
247
+
248
+ Examples
249
+ --------
250
+ >>> extract_year('uncertainty (2050)')
251
+ 2050
252
+
253
+ """
254
+ # Extract digits
255
+ digits = re.findall(r"\d+", year_str)
256
+
257
+ # Convert to integer
258
+ return int(digits[0]) if digits else None
259
+
260
+
261
+ @pydantic.validate_call
262
+ def clean_est_string(est_str: str) -> str:
263
+ """
264
+ Casefold the 'est' string, trim whitespace and replace `ctrl` with `control`.
265
+
266
+ Parameters
267
+ ----------
268
+ est_str : str
269
+ The input 'est' string to be cleaned.
270
+
271
+ Returns
272
+ -------
273
+ str
274
+ The cleaned 'est' string.
275
+
276
+ Examples
277
+ --------
278
+ >>> clean_est_string("Lower")
279
+ lower
280
+ >>> clean_est_string("ctrl")
281
+ control
282
+
283
+ """
284
+ if est_str == "ctrl":
285
+ cleaned_str = "control"
286
+ else:
287
+ cleaned_str = est_str.casefold().strip()
288
+ return cleaned_str
289
+
290
+
291
+ def standardize_units(series: pd.Series) -> pd.Series:
292
+ """
293
+ Complete missing units based on parameter names and replace incorrect units.
294
+
295
+ Parameters
296
+ ----------
297
+ series : pandas.Series
298
+ A series containing two elements: [par, unit]
299
+
300
+ Returns
301
+ -------
302
+ pandas.Series
303
+ Updated series with completed and corrected unit.
304
+
305
+ Notes
306
+ -----
307
+ The following substitutions are driven by the `pint` documentation available
308
+ at https://github.com/hgrecco/pint/blob/master/pint/default_en.txt:
309
+ - "pct.": "percent",
310
+ - "m2": "meter**2",
311
+ - "m3": "meter**3",
312
+
313
+ """
314
+ par, unit = series
315
+
316
+ # Mapping of parameters to their default units
317
+ param_unit_map = {
318
+ "energy storage capacity for one unit": "MWh",
319
+ "typical temperature difference in storage": "K",
320
+ "fixed o&m": "pct./year",
321
+ "lifetime in total number of cycles": "cycles",
322
+ "cycle life": "cycles",
323
+ }
324
+
325
+ # Mapping of incorrect units to correct units
326
+ unit_corrections = {
327
+ "pct./period": "percent",
328
+ "⁰C": "C",
329
+ "°C": "C",
330
+ "pct./30sec": "pct.",
331
+ "m2": "meter**2",
332
+ "m3": "meter**3",
333
+ "MWhoutput": "MWh",
334
+ "hot/cold,K": "K",
335
+ "pct.investement": "percent",
336
+ "pct.investment": "percent",
337
+ "tank/": "",
338
+ "pct.": "percent",
339
+ }
340
+
341
+ # Complete missing or empty units
342
+ if (not isinstance(unit, str)) or (unit.strip() == ""):
343
+ unit = param_unit_map.get(par, unit)
344
+
345
+ # Replace wrong units
346
+ for incorrect, correct in unit_corrections.items():
347
+ if incorrect == unit or incorrect in unit:
348
+ unit = unit.replace(incorrect, correct)
349
+
350
+ return pd.Series([par, unit])
351
+
352
+
353
+ def filter_parameters(dataframe: pd.DataFrame, filter_flag: bool) -> pd.DataFrame:
354
+ """
355
+ Filter rows of a DataFrame by allowed technology parameters.
356
+
357
+ Parameters
358
+ ----------
359
+ dataframe : pandas.DataFrame
360
+ Input DataFrame containing at least a "Technology" column.
361
+ filter_flag : Boolean
362
+ If true, filter parameter `par` column
363
+
364
+ Returns
365
+ -------
366
+ pandas.DataFrame
367
+ The filtered DataFrame. The returned
368
+ DataFrame contains only rows where the `par` column is one of:
369
+ "technical lifetime", "fixed o&m", "specific investment", or
370
+ "variable o&m".
371
+
372
+ """
373
+ allowed_set = {
374
+ "technical lifetime",
375
+ "fixed o&m",
376
+ "specific investment",
377
+ "variable o&m",
378
+ "charge efficiency",
379
+ "discharge efficiency",
380
+ "capacity",
381
+ }
382
+ print("filter_flag", filter_flag)
383
+ if filter_flag:
384
+ # Filter the DataFrame based on the allowed set
385
+ df_filtered = dataframe[dataframe["par"].isin(allowed_set)].reset_index(
386
+ drop=True
387
+ )
388
+ logger.info(
389
+ f"technologies.json contains a subset of the allowed parameters: {allowed_set}."
390
+ )
391
+ else:
392
+ # Return the original DataFrame if filter_flag is False
393
+ df_filtered = dataframe
394
+ logger.info("All parameters are outputted to technologies.json")
395
+ return df_filtered
396
+
397
+
398
+ def build_technology_collection(
399
+ dataframe: pd.DataFrame,
400
+ sources_path: pathlib.Path,
401
+ store_source: bool = False,
402
+ output_schema: bool = False,
403
+ ) -> TechnologyCollection:
404
+ """
405
+ Compute a collection of technologies from a grouped DataFrame.
406
+
407
+ Processes input DataFrame by grouping technologies and extracting their parameters,
408
+ creating Technology instances for each unique group.
409
+
410
+ Parameters
411
+ ----------
412
+ dataframe : pandas.DataFrame
413
+ Input DataFrame containing technology parameters.
414
+ Expected columns include:
415
+ - 'est': Estimation or case identifier
416
+ - 'year': Year of the technology
417
+ - 'ws': Workspace or technology identifier
418
+ - 'Technology': Detailed technology name
419
+ - 'par': Parameter name
420
+ - 'val': Parameter value
421
+ - 'unit': Parameter units
422
+ sources_path: pathlib.Path
423
+ Output path for storing the SourceCollection object
424
+ store_source: Optional[bool]
425
+ Flag to decide whether to store the source object on the Wayback Machine. Default False.
426
+ output_schema : Optional[bool]
427
+ Flag to decide whether to export the source collection schema. Default False.
428
+
429
+ Returns
430
+ -------
431
+ TechnologyCollection
432
+ A collection of Technology instances, each representing a unique
433
+ technology group with its associated parameters.
434
+
435
+ Notes
436
+ -----
437
+ - The function groups the DataFrame by 'est', 'year', 'ws', and 'Technology'
438
+ - For each group, it creates a dictionary of Parameters
439
+ - Each Technology is instantiated with group-specific attributes
440
+
441
+ """
442
+ list_techs = []
443
+
444
+ if store_source:
445
+ source = Source(
446
+ title="Technology Data for Energy storage (May 2025)",
447
+ authors="Danish Energy Agency",
448
+ url="https://ens.dk/media/6589/download",
449
+ url_date="2025-10-08 09:24:00",
450
+ )
451
+ source.ensure_in_wayback()
452
+ sources = SourceCollection(sources=[source])
453
+ sources.to_json(sources_path, output_schema=output_schema)
454
+ else:
455
+ sources = SourceCollection.from_json(sources_path)
456
+
457
+ for (est, year, ws, technology_name), group in dataframe.groupby(
458
+ ["est", "year", "ws", "Technology"]
459
+ ):
460
+ parameters = {}
461
+ for _, row in group.iterrows():
462
+ parameters[row["par"]] = Parameter(
463
+ magnitude=row["val"],
464
+ units=row["unit"],
465
+ sources=sources,
466
+ provenance="Parsed from Excel file",
467
+ )
468
+ list_techs.append(
469
+ Technology(
470
+ name=ws,
471
+ region="EU",
472
+ year=year,
473
+ parameters=parameters,
474
+ case=est,
475
+ detailed_technology=technology_name,
476
+ )
477
+ )
478
+ return TechnologyCollection(technologies=list_techs)
479
+
480
+
481
+ @pydantic.validate_call
482
+ def parse_input_arguments() -> argparse.Namespace:
483
+ """
484
+ Parse command line arguments.
485
+
486
+ Returns
487
+ -------
488
+ argparse.Namespace
489
+ Parsed command line arguments containing:
490
+ - Number of significant digits
491
+ - Store source flag
492
+
493
+ """
494
+ # Create the parser
495
+ parser = argparse.ArgumentParser(
496
+ description="Parse the DEA technology storage dataset",
497
+ formatter_class=argparse.RawTextHelpFormatter,
498
+ )
499
+
500
+ # Define arguments
501
+ parser.add_argument(
502
+ "--num_digits",
503
+ type=int,
504
+ default=4,
505
+ help="Name of significant digits to round the values. ",
506
+ )
507
+
508
+ parser.add_argument(
509
+ "--store_source",
510
+ action="store_true",
511
+ help="store_source, store the source object on the wayback machine. Default: false",
512
+ )
513
+
514
+ parser.add_argument(
515
+ "--filter_params",
516
+ action="store_true",
517
+ help="filter_params. Filter the parameters stored to technologies.json. Default: false",
518
+ )
519
+
520
+ parser.add_argument(
521
+ "--export_schema",
522
+ action="store_true",
523
+ help="export_schema. Export the Source/TechnologyCollection schemas. Default: false",
524
+ )
525
+
526
+ # Parse arguments
527
+ args = parser.parse_args()
528
+
529
+ return args
530
+
531
+
532
+ if __name__ == "__main__":
533
+ # Parse input arguments
534
+ input_args = parse_input_arguments()
535
+ logger.info("Command line arguments parsed.")
536
+
537
+ # Read the raw data
538
+ dea_energy_storage_file_path = pathlib.Path(
539
+ path_cwd,
540
+ "src",
541
+ "technologydata",
542
+ "package_data",
543
+ "raw",
544
+ "Technology_datasheet_for_energy_storage.xlsx",
545
+ )
546
+
547
+ dea_energy_storage_df = pd.read_excel(
548
+ dea_energy_storage_file_path,
549
+ sheet_name="alldata_flat",
550
+ engine="calamine",
551
+ dtype=str,
552
+ )
553
+ logger.info("Input file read-in.")
554
+
555
+ # Drop unnecessary rows
556
+ cleaned_df = drop_invalid_rows(dea_energy_storage_df)
557
+ logger.info("Unnecessary rows dropped.")
558
+
559
+ # Clean technology (Technology) column
560
+ cleaned_df["Technology"] = cleaned_df["Technology"].apply(clean_technology_string)
561
+
562
+ # Clean ws column
563
+ cleaned_df["ws"] = cleaned_df["ws"].apply(clean_technology_string)
564
+ logger.info("`Technology` and `ws` cleaned.")
565
+
566
+ # Clean year column
567
+ cleaned_df["year"] = cleaned_df["year"].apply(extract_year)
568
+ logger.info("`year` column cleaned.")
569
+
570
+ # Clean parameter (par) column
571
+ cleaned_df["par"] = cleaned_df["par"].apply(clean_parameter_string)
572
+ logger.info("`par` column cleaned.")
573
+
574
+ # Complete missing units based on parameter names and replace incorrect units.
575
+ cleaned_df[["par", "unit"]] = cleaned_df[["par", "unit"]].apply(
576
+ standardize_units, axis=1
577
+ )
578
+ logger.info("Missing units added and wrong units replaced.")
579
+
580
+ # Include priceyear in unit if applicable
581
+ cleaned_df["unit"] = cleaned_df.apply(
582
+ lambda row: Commons.update_unit_with_currency_year(
583
+ row["unit"], row["priceyear"]
584
+ ),
585
+ axis=1,
586
+ )
587
+ logger.info("`priceyear` included in `unit` column.")
588
+
589
+ # Format value (val) column
590
+ cleaned_df["val"] = cleaned_df["val"].apply(
591
+ lambda x: format_val_number(x, input_args.num_digits)
592
+ )
593
+ logger.info("`val` column formatted.")
594
+
595
+ # Replace "MEUR_2020" with "EUR_2020" and multiply val by 1_000_000
596
+ mask_meur = cleaned_df["unit"].str.contains("MEUR_2020")
597
+ cleaned_df.loc[mask_meur, "unit"] = cleaned_df.loc[mask_meur, "unit"].str.replace(
598
+ "MEUR_2020", "EUR_2020"
599
+ )
600
+ cleaned_df.loc[mask_meur, "val"] = (
601
+ cleaned_df.loc[mask_meur, "val"] * 1_000_000.0
602
+ ).round(input_args.num_digits)
603
+
604
+ # Replace "kEUR_2020" with "EUR_2020" and multiply val by 1_000
605
+ mask_lower_keur = cleaned_df["unit"].str.contains("kEUR_2020")
606
+ cleaned_df.loc[mask_lower_keur, "unit"] = cleaned_df.loc[
607
+ mask_lower_keur, "unit"
608
+ ].str.replace("kEUR_2020", "EUR_2020")
609
+ cleaned_df.loc[mask_lower_keur, "val"] = (
610
+ cleaned_df.loc[mask_lower_keur, "val"] * 1_000.0
611
+ ).round(input_args.num_digits)
612
+
613
+ # Replace "KEUR_2020" with "EUR_2020" and multiply val by 1_000
614
+ mask_upper_keur = cleaned_df["unit"].str.contains("KEUR_2020")
615
+ cleaned_df.loc[mask_upper_keur, "unit"] = cleaned_df.loc[
616
+ mask_upper_keur, "unit"
617
+ ].str.replace("KEUR_2020", "EUR_2020")
618
+ cleaned_df.loc[mask_upper_keur, "val"] = (
619
+ cleaned_df.loc[mask_upper_keur, "val"] * 1_000.0
620
+ ).round(input_args.num_digits)
621
+
622
+ # Replace "mol/s/m/MPa1/2" with "mol/s/m/Pa" and multiply val by 1_000_000
623
+ mask_mols = cleaned_df["unit"].str.contains("mol/s/m/MPa1/2")
624
+ cleaned_df.loc[mask_mols, "unit"] = cleaned_df.loc[mask_mols, "unit"].str.replace(
625
+ "mol/s/m/MPa1/2", "mol/s/m/Pa"
626
+ )
627
+ cleaned_df.loc[mask_mols, "val"] = cleaned_df.loc[mask_mols, "val"] * 1_000_000.0
628
+
629
+ # Replace, in column `par`, `energy storage capacity for one unit` and `tank volume of example` with `capacity`
630
+ mask_capacity = cleaned_df["par"].isin(
631
+ [
632
+ "energy storage capacity for one unit",
633
+ "tank volume of example",
634
+ ]
635
+ )
636
+ cleaned_df.loc[mask_capacity, "par"] = "capacity"
637
+
638
+ # Clean est column
639
+ cleaned_df["est"] = cleaned_df["est"].apply(clean_est_string)
640
+ logger.info("`est` column cleaned.")
641
+
642
+ # Drop unnecessary columns
643
+ columns_to_drop = ["cat", "priceyear", "ref", "note"]
644
+ cleaned_df = cleaned_df.drop(columns=columns_to_drop, errors="ignore")
645
+ logger.info("Unnecessary columns dropped.")
646
+
647
+ filtered_df = filter_parameters(cleaned_df, input_args.filter_params)
648
+
649
+ # Build TechnologyCollection
650
+ dea_storage_path = pathlib.Path(
651
+ path_cwd,
652
+ "src",
653
+ "technologydata",
654
+ "package_data",
655
+ "dea_energy_storage",
656
+ )
657
+ output_technologies_path = pathlib.Path(
658
+ dea_storage_path,
659
+ "technologies.json",
660
+ )
661
+ output_sources_path = pathlib.Path(
662
+ dea_storage_path,
663
+ "sources.json",
664
+ )
665
+
666
+ tech_col = build_technology_collection(
667
+ filtered_df,
668
+ output_sources_path,
669
+ store_source=input_args.store_source,
670
+ output_schema=input_args.export_schema,
671
+ )
672
+ logger.info("TechnologyCollection object instantiated.")
673
+ tech_col.to_json(output_technologies_path, output_schema=input_args.export_schema)
674
+ logger.info("TechnologyCollection object exported to json.")
675
+
676
+ if input_args.export_schema:
677
+ # Move schema files if they exist
678
+ schema_folder = pathlib.Path(
679
+ path_cwd, "src", "technologydata", "package_data", "schemas"
680
+ )
681
+ sources_schema = pathlib.Path(dea_storage_path, "sources.schema.json")
682
+ technologies_schema = pathlib.Path(dea_storage_path, "technologies.schema.json")
683
+
684
+ schema_folder.mkdir(parents=True, exist_ok=True)
685
+
686
+ if sources_schema.exists():
687
+ sources_schema.rename(schema_folder / "sources.schema.json")
688
+ logger.info(f"Moved {sources_schema} to {schema_folder}")
689
+ if technologies_schema.exists():
690
+ technologies_schema.rename(schema_folder / "technologies.schema.json")
691
+ logger.info(f"Moved {technologies_schema} to {schema_folder}")
@@ -0,0 +1,12 @@
1
+ {
2
+ "sources": [
3
+ {
4
+ "title": "Technology Data for Energy storage (May 2025)",
5
+ "authors": "Danish Energy Agency",
6
+ "url": "https://ens.dk/media/6589/download",
7
+ "url_archive": "https://web.archive.org/web/20251008092400/https://ens.dk/media/6589/download",
8
+ "url_date": "2025-10-08 09:24:00",
9
+ "url_date_archive": "2025-10-08 09:24:00"
10
+ }
11
+ ]
12
+ }