deep-code 0.0.1.dev0__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,63 @@
1
+ import unittest
2
+ from datetime import datetime, timezone
3
+
4
+ from deep_code.constants import DEFAULT_THEME_SCHEME
5
+ from deep_code.utils.ogc_record_generator import OSCWorkflowOGCApiRecordGenerator
6
+
7
+
8
+ class TestOSCWorkflowOGCApiRecordGenerator(unittest.TestCase):
9
+ def test_build_contact_objects(self):
10
+ contacts_list = [
11
+ {"name": "Alice", "organization": "Org A", "position": "Researcher"},
12
+ {"name": "Bob", "organization": "Org B", "position": "Developer"},
13
+ ]
14
+
15
+ result = OSCWorkflowOGCApiRecordGenerator.build_contact_objects(contacts_list)
16
+
17
+ self.assertEqual(len(result), 2)
18
+ self.assertEqual(result[0].name, "Alice")
19
+ self.assertEqual(result[0].organization, "Org A")
20
+ self.assertEqual(result[0].position, "Researcher")
21
+ self.assertEqual(result[1].name, "Bob")
22
+ self.assertEqual(result[1].organization, "Org B")
23
+ self.assertEqual(result[1].position, "Developer")
24
+
25
+ def test_build_theme(self):
26
+ osc_themes = ["theme1", "theme2"]
27
+
28
+ theme = OSCWorkflowOGCApiRecordGenerator.build_theme(osc_themes)
29
+
30
+ self.assertEqual(len(theme.concepts), 2)
31
+ self.assertEqual(theme.concepts[0].id, "theme1")
32
+ self.assertEqual(theme.concepts[1].id, "theme2")
33
+ self.assertEqual(theme.scheme, DEFAULT_THEME_SCHEME)
34
+
35
+ def test_build_record_properties(self):
36
+ generator = OSCWorkflowOGCApiRecordGenerator()
37
+ properties = {
38
+ "title": "Test Workflow",
39
+ "description": "A test description",
40
+ "themes": ["theme1"],
41
+ "jupyter_kernel_info": {
42
+ "name": "deepesdl-xcube-1.7.1",
43
+ "python_version": 3.11,
44
+ "env_file": "https://git/env.yml",
45
+ },
46
+ }
47
+ contacts = [
48
+ {"name": "Alice", "organization": "Org A", "position": "Researcher"}
49
+ ]
50
+
51
+ record_properties = generator.build_record_properties(properties, contacts)
52
+
53
+ now_iso = datetime.now(timezone.utc).isoformat()
54
+
55
+ self.assertEqual(record_properties.title, "Test Workflow")
56
+ self.assertEqual(record_properties.description, "A test description")
57
+ self.assertEqual(len(record_properties.contacts), 1)
58
+ self.assertEqual(record_properties.contacts[0].name, "Alice")
59
+ self.assertEqual(len(record_properties.themes), 1)
60
+ self.assertEqual(record_properties.themes[0].concepts[0].id, "theme1")
61
+ self.assertEqual(record_properties.type, "workflow")
62
+ self.assertTrue("created" in record_properties.__dict__)
63
+ self.assertTrue("updated" in record_properties.__dict__)
@@ -0,0 +1,117 @@
1
+ import unittest
2
+
3
+ from pystac import Collection, Extent, SpatialExtent, TemporalExtent
4
+
5
+ from deep_code.utils.osc_extension import OscExtension
6
+
7
+
8
+ class TestOscExtension(unittest.TestCase):
9
+ def setUp(self):
10
+ """Set up a test Collection object and attach the OscExtension."""
11
+ self.collection = Collection(
12
+ id="test-collection",
13
+ description="Test collection for unit tests",
14
+ extent=Extent(
15
+ spatial=SpatialExtent([[-180, -90, 180, 90]]),
16
+ temporal=TemporalExtent(
17
+ [["2022-01-01T00:00:00Z", "2023-01-01T00:00:00Z"]]
18
+ ),
19
+ ),
20
+ stac_extensions=[],
21
+ )
22
+ OscExtension.add_to(self.collection)
23
+
24
+ def test_osc_status(self):
25
+ """Test the osc:status property."""
26
+ extension = OscExtension.ext(self.collection)
27
+ extension.osc_status = "ongoing"
28
+ self.assertEqual(extension.osc_status, "ongoing")
29
+
30
+ def test_osc_region(self):
31
+ """Test the osc:region property."""
32
+ extension = OscExtension.ext(self.collection)
33
+ extension.osc_region = "Mediterranean region"
34
+ self.assertEqual(extension.osc_region, "Mediterranean region")
35
+
36
+ def test_osc_themes(self):
37
+ """Test the osc:themes property."""
38
+ extension = OscExtension.ext(self.collection)
39
+ extension.osc_themes = ["land", "ocean"]
40
+ self.assertEqual(extension.osc_themes, ["land", "ocean"])
41
+
42
+ def test_osc_missions(self):
43
+ """Test the osc:missions property."""
44
+ extension = OscExtension.ext(self.collection)
45
+ extension.osc_missions = ["mission1", "mission2"]
46
+ self.assertEqual(extension.osc_missions, ["mission1", "mission2"])
47
+
48
+ def test_keywords(self):
49
+ """Test the keywords property."""
50
+ extension = OscExtension.ext(self.collection)
51
+ extension.keywords = ["Hydrology", "Remote Sensing"]
52
+ self.assertEqual(extension.keywords, ["Hydrology", "Remote Sensing"])
53
+
54
+ def test_cf_parameters(self):
55
+ """Test the cf:parameter property."""
56
+ extension = OscExtension.ext(self.collection)
57
+ extension.cf_parameter = [{"name": "hydrology-4D"}]
58
+ self.assertEqual(extension.cf_parameter, [{"name": "hydrology-4D"}])
59
+
60
+ def test_created_updated(self):
61
+ """Test the created and updated properties."""
62
+ extension = OscExtension.ext(self.collection)
63
+ extension.created = "2023-12-21T11:50:17Z"
64
+ extension.updated = "2023-12-21T11:50:17Z"
65
+ self.assertEqual(extension.created, "2023-12-21T11:50:17Z")
66
+ self.assertEqual(extension.updated, "2023-12-21T11:50:17Z")
67
+
68
+ def test_set_extent(self):
69
+ """Test setting spatial and temporal extent."""
70
+ extension = OscExtension.ext(self.collection)
71
+ spatial = [[-5.7, 28.3, 37.7, 48.1]]
72
+ temporal = [["2014-12-31T12:00:00Z", "2022-10-06T12:00:00Z"]]
73
+ extension.set_extent(spatial, temporal)
74
+
75
+ self.assertEqual(self.collection.extent.spatial.bboxes, spatial)
76
+ self.assertEqual(self.collection.extent.temporal.intervals, temporal)
77
+
78
+ def test_validation_success(self):
79
+ """Test validation with all required fields."""
80
+ extension = OscExtension.ext(self.collection)
81
+ extension.osc_type = "product"
82
+ extension.osc_project = "test-project"
83
+ extension.osc_status = "ongoing"
84
+ extension.validate_extension() # Should not raise an exception
85
+
86
+ def test_add_osc_extension(self):
87
+ osc_ext = OscExtension.add_to(self.collection)
88
+ self.assertEqual(OscExtension.get_schema_uri(), self.collection.stac_extensions)
89
+ self.assertIsInstance(osc_ext, OscExtension)
90
+
91
+ def test_has_extension(self):
92
+ self.collection.stac_extensions = []
93
+ self.assertFalse(OscExtension.has_extension(self.collection))
94
+ OscExtension.add_to(self.collection)
95
+ self.assertTrue(OscExtension.has_extension(self.collection))
96
+
97
+ def test_set_and_get_properties(self):
98
+ osc_ext = OscExtension.add_to(self.collection)
99
+ osc_ext.osc_type = "example-type"
100
+ osc_ext.osc_project = "example-project"
101
+ osc_ext.osc_product = "example-product"
102
+ osc_ext.osc_theme = ["example-theme"]
103
+ osc_ext.osc_variables = ["var1", "var2", "var3"]
104
+
105
+ self.assertEqual(osc_ext.osc_type, "example-type")
106
+ self.assertEqual(osc_ext.osc_project, "example-project")
107
+ self.assertEqual(osc_ext.osc_product, "example-product")
108
+ self.assertEqual(osc_ext.osc_theme, ["example-theme"])
109
+ self.assertListEqual(osc_ext.osc_variables, ["var1", "var2", "var3"])
110
+
111
+ def test_validation_missing_fields(self):
112
+ """Test validation with missing required fields."""
113
+ extension = OscExtension.ext(self.collection)
114
+ with self.assertRaises(ValueError) as context:
115
+ extension.validate_extension()
116
+ self.assertIn("Missing required fields", str(context.exception))
117
+ self.assertIn("osc:type", str(context.exception))
@@ -0,0 +1,3 @@
1
+ # Copyright (c) 2025 by Brockmann Consult GmbH
2
+ # Permissions are hereby granted under the terms of the MIT License:
3
+ # https://opensource.org/licenses/MIT.
@@ -0,0 +1,4 @@
1
+ """
2
+ Verify the readiness of a dataset or an existing workflow repository for experiment
3
+ publication by identifying any issues or missing components
4
+ """
deep_code/tools/new.py ADDED
@@ -0,0 +1,5 @@
1
+ """Logic for initializing repositories
2
+ Initialize a GitHub repository with the proposed configurations files, an initial
3
+ workflow notebook template (e.g. workflow.ipynb), a template Python package (code and
4
+ pyproject.toml), and a template setup for documentation (e.g., using mkdocs),
5
+ setup of thebuild pipeline"""
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # Copyright (c) 2025 by Brockmann Consult GmbH
4
+ # Permissions are hereby granted under the terms of the MIT License:
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ import logging
8
+ from pathlib import Path
9
+
10
+ import fsspec
11
+ import yaml
12
+
13
+ from deep_code.constants import (
14
+ OSC_BRANCH_NAME,
15
+ OSC_REPO_NAME,
16
+ OSC_REPO_OWNER,
17
+ WF_BRANCH_NAME,
18
+ )
19
+ from deep_code.utils.dataset_stac_generator import OscDatasetStacGenerator
20
+ from deep_code.utils.github_automation import GitHubAutomation
21
+ from deep_code.utils.ogc_api_record import OgcRecord
22
+ from deep_code.utils.ogc_record_generator import OSCWorkflowOGCApiRecordGenerator
23
+
24
+ logger = logging.getLogger(__name__)
25
+ logging.basicConfig(level=logging.INFO)
26
+
27
+
28
+ class GitHubPublisher:
29
+ """
30
+ Base class providing:
31
+ - Reading .gitaccess for credentials
32
+ - Common GitHub automation steps (fork, clone, branch, file commit, pull request)
33
+ """
34
+
35
+ def __init__(self):
36
+ with fsspec.open(".gitaccess", "r") as file:
37
+ git_config = yaml.safe_load(file) or {}
38
+ self.github_username = git_config.get("github-username")
39
+ self.github_token = git_config.get("github-token")
40
+ if not self.github_username or not self.github_token:
41
+ raise ValueError("GitHub credentials are missing in the `.gitaccess` file.")
42
+
43
+ self.github_automation = GitHubAutomation(
44
+ self.github_username, self.github_token, OSC_REPO_OWNER, OSC_REPO_NAME
45
+ )
46
+
47
+ def publish_files(
48
+ self,
49
+ branch_name: str,
50
+ file_dict: dict[str, dict],
51
+ commit_message: str,
52
+ pr_title: str,
53
+ pr_body: str,
54
+ ) -> str:
55
+ """Publish multiple files to a new branch and open a PR.
56
+
57
+ Args:
58
+ branch_name: Branch name to create (e.g. "osc-branch-collectionid").
59
+ file_dict: { file_path: file_content_dict } for each file to commit.
60
+ commit_message: Commit message for all changes.
61
+ pr_title: Title of the pull request.
62
+ pr_body: Description/body of the pull request.
63
+
64
+ Returns:
65
+ URL of the created pull request.
66
+ """
67
+ try:
68
+ logger.info("Forking and cloning repository...")
69
+ self.github_automation.fork_repository()
70
+ self.github_automation.clone_repository()
71
+ self.github_automation.create_branch(branch_name)
72
+
73
+ # Add each file to the branch
74
+ for file_path, content in file_dict.items():
75
+ logger.info(f"Adding {file_path} to {branch_name}")
76
+ self.github_automation.add_file(file_path, content)
77
+
78
+ # Commit and push
79
+ self.github_automation.commit_and_push(branch_name, commit_message)
80
+
81
+ # Create pull request
82
+ pr_url = self.github_automation.create_pull_request(
83
+ branch_name, pr_title, pr_body
84
+ )
85
+ logger.info(f"Pull request created: {pr_url}")
86
+ return pr_url
87
+
88
+ finally:
89
+ # Cleanup local clone
90
+ self.github_automation.clean_up()
91
+
92
+
93
+ class DatasetPublisher:
94
+ """Publishes products (datasets) to the OSC GitHub repository.
95
+ Inherits from BasePublisher for GitHub publishing logic.
96
+ """
97
+
98
+ def __init__(self):
99
+ # Composition
100
+ self.gh_publisher = GitHubPublisher()
101
+
102
+ def publish_dataset(self, dataset_config_path: str):
103
+ """Publish a product collection to the specified GitHub repository."""
104
+ with fsspec.open(dataset_config_path, "r") as file:
105
+ dataset_config = yaml.safe_load(file) or {}
106
+
107
+ dataset_id = dataset_config.get("dataset_id")
108
+ collection_id = dataset_config.get("collection_id")
109
+ documentation_link = dataset_config.get("documentation_link")
110
+ access_link = dataset_config.get("access_link")
111
+ dataset_status = dataset_config.get("dataset_status")
112
+ osc_region = dataset_config.get("osc_region")
113
+ osc_themes = dataset_config.get("osc_themes")
114
+ cf_params = dataset_config.get("cf_parameter")
115
+
116
+ if not dataset_id or not collection_id:
117
+ raise ValueError("Dataset ID or Collection ID missing in the config.")
118
+
119
+ logger.info("Generating STAC collection...")
120
+
121
+ generator = OscDatasetStacGenerator(
122
+ dataset_id=dataset_id,
123
+ collection_id=collection_id,
124
+ documentation_link=documentation_link,
125
+ access_link=access_link,
126
+ osc_status=dataset_status,
127
+ osc_region=osc_region,
128
+ osc_themes=osc_themes,
129
+ cf_params=cf_params,
130
+ )
131
+
132
+ variable_ids = generator.get_variable_ids()
133
+ ds_collection = generator.build_dataset_stac_collection()
134
+
135
+ # Prepare a dictionary of file paths and content
136
+ file_dict = {}
137
+ product_path = f"products/{collection_id}/collection.json"
138
+ file_dict[product_path] = ds_collection.to_dict()
139
+
140
+ # Add or update variable files
141
+ for var_id in variable_ids:
142
+ var_file_path = f"variables/{var_id}/catalog.json"
143
+ if not self.gh_publisher.github_automation.file_exists(var_file_path):
144
+ logger.info(
145
+ f"Variable catalog for {var_id} does not exist. Creating..."
146
+ )
147
+ var_metadata = generator.variables_metadata.get(var_id)
148
+ var_catalog = generator.build_variable_catalog(var_metadata)
149
+ file_dict[var_file_path] = var_catalog.to_dict()
150
+ else:
151
+ logger.info(
152
+ f"Variable catalog already exists for {var_id}, adding product link."
153
+ )
154
+ full_path = (
155
+ Path(self.gh_publisher.github_automation.local_clone_dir)
156
+ / var_file_path
157
+ )
158
+ updated_catalog = generator.update_existing_variable_catalog(
159
+ full_path, var_id
160
+ )
161
+ file_dict[var_file_path] = updated_catalog.to_dict()
162
+
163
+ # Create branch name, commit message, PR info
164
+ branch_name = f"{OSC_BRANCH_NAME}-{collection_id}"
165
+ commit_message = f"Add new dataset collection: {collection_id}"
166
+ pr_title = "Add new dataset collection"
167
+ pr_body = "This PR adds a new dataset collection to the repository."
168
+
169
+ # Publish all files in one go
170
+ pr_url = self.gh_publisher.publish_files(
171
+ branch_name=branch_name,
172
+ file_dict=file_dict,
173
+ commit_message=commit_message,
174
+ pr_title=pr_title,
175
+ pr_body=pr_body,
176
+ )
177
+
178
+ logger.info(f"Pull request created: {pr_url}")
179
+
180
+
181
+ class WorkflowPublisher:
182
+ """Publishes workflows to the OSC GitHub repository."""
183
+
184
+ def __init__(self):
185
+ self.gh_publisher = GitHubPublisher()
186
+
187
+ @staticmethod
188
+ def _normalize_name(name: str | None) -> str | None:
189
+ return name.replace(" ", "-").lower() if name else None
190
+
191
+ def publish_workflow(self, workflow_config_path: str):
192
+ with fsspec.open(workflow_config_path, "r") as file:
193
+ workflow_config = yaml.safe_load(file) or {}
194
+
195
+ workflow_id = self._normalize_name(workflow_config.get("workflow_id"))
196
+ if not workflow_id:
197
+ raise ValueError("workflow_id is missing in workflow config.")
198
+
199
+ properties_list = workflow_config.get("properties", [])
200
+ contacts = workflow_config.get("contact", [])
201
+ links = workflow_config.get("links", [])
202
+
203
+ logger.info("Generating OGC API Record for the workflow...")
204
+ rg = OSCWorkflowOGCApiRecordGenerator()
205
+ wf_record_properties = rg.build_record_properties(properties_list, contacts)
206
+
207
+ ogc_record = OgcRecord(
208
+ id=workflow_id,
209
+ type="Feature",
210
+ time={},
211
+ properties=wf_record_properties,
212
+ links=links,
213
+ )
214
+
215
+ file_path = f"workflow/{workflow_id}/collection.json"
216
+
217
+ # Prepare the single file dict
218
+ file_dict = {file_path: ogc_record.to_dict()}
219
+
220
+ branch_name = f"{WF_BRANCH_NAME}-{workflow_id}"
221
+ commit_message = f"Add new workflow: {workflow_id}"
222
+ pr_title = "Add new workflow"
223
+ pr_body = "This PR adds a new workflow to the OSC repository."
224
+
225
+ pr_url = self.gh_publisher.publish_files(
226
+ branch_name=branch_name,
227
+ file_dict=file_dict,
228
+ commit_message=commit_message,
229
+ pr_title=pr_title,
230
+ pr_body=pr_body,
231
+ )
232
+
233
+ logger.info(f"Pull request created: {pr_url}")
File without changes
@@ -0,0 +1 @@
1
+ """Logic for setting up build pipelines"""
@@ -0,0 +1,2 @@
1
+ """ Execute the application package of a published experiment on a subset of input data
2
+ to verify the reproducibility is achieved"""
@@ -0,0 +1,3 @@
1
+ # Copyright (c) 2025 by Brockmann Consult GmbH
2
+ # Permissions are hereby granted under the terms of the MIT License:
3
+ # https://opensource.org/licenses/MIT.