Skip to content

Commit 6daccd9

Browse files
authored
Merge pull request #95 from Exabyte-io/feature/SOF-7755
Feature/SOF-7755 Add Python codebase
2 parents e4655a5 + 5e41179 commit 6daccd9

18 files changed

+1127
-18
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,19 @@ For usage within a JavaScript project:
2222
npm install @mat3ra/ade
2323
```
2424

25+
For usage within a Python project:
26+
27+
```bash
28+
pip install mat3ra-ade
29+
```
30+
2531
For development:
2632

2733
```bash
2834
git clone https://github.com/Exabyte-io/ade.git
2935
```
3036

37+
3138
## Contributions
3239

3340
This repository is an [open-source](LICENSE.md) work-in-progress and we welcome contributions.
@@ -39,6 +46,7 @@ See [ESSE](https://github.com/Exabyte-io/esse) for additional context regarding
3946

4047
Useful commands for development:
4148

49+
### JavaScript/TypeScript
4250
```bash
4351
# run linter without persistence
4452
npm run lint
@@ -62,6 +70,20 @@ npm run test:coverage:check
6270
npm run test:coverage:html
6371
```
6472

73+
### Python
74+
```bash
75+
# run linter
76+
python -m black src/py/mat3ra/ade/ tests/py/
77+
python -m ruff check src/py/mat3ra/ade/ tests/py/
78+
python -m isort src/py/mat3ra/ade/ tests/py/
79+
80+
# run tests
81+
python -m pytest tests/py/
82+
83+
# run tests with coverage
84+
python -m pytest tests/py/ --cov=mat3ra.ade --cov-report=html
85+
```
86+
6587
## Development: Code/Test Coverage
6688

6789
This project includes comprehensive code coverage reporting with multiple viewing options:

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ classifiers = [
2020
]
2121
dependencies = [
2222
"numpy",
23+
"pydantic>=2.0",
24+
"mat3ra-esse",
25+
"mat3ra-code",
26+
"mat3ra-utils[extra]"
2327
]
2428

2529
[project.optional-dependencies]
@@ -35,6 +39,8 @@ tests = [
3539
"coverage[toml]>=5.3",
3640
"pytest",
3741
"pytest-cov",
42+
"mat3ra-esse",
43+
"mat3ra-utils"
3844
]
3945
all = [
4046
"mat3ra-ade[tests]",

src/py/mat3ra/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
"""mat3ra namespace package."""
2-
1+
__path__ = __import__("pkgutil").extend_path(__path__, __name__)

src/py/mat3ra/ade/__init__.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
1-
import numpy as np
2-
3-
4-
def get_length(vec: np.ndarray) -> float:
5-
return float(np.linalg.norm(vec))
1+
from mat3ra.ade.application import Application
2+
from mat3ra.ade.executable import Executable
3+
from mat3ra.ade.flavor import Flavor, FlavorInput
4+
from mat3ra.ade.template import (
5+
ContextProvider,
6+
Template,
7+
)
8+
from mat3ra.ade.context.json_schema_data_provider import (
9+
JSONSchemaDataProvider,
10+
)
11+
from mat3ra.ade.context.jinja_context_provider import JinjaContextProvider
612

13+
__all__ = [
14+
"Application",
15+
"Executable",
16+
"Flavor",
17+
"FlavorInput",
18+
"Template",
19+
"ContextProvider",
20+
"JinjaContextProvider",
21+
"JSONSchemaDataProvider",
22+
]

src/py/mat3ra/ade/application.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from mat3ra.code.entity import InMemoryEntitySnakeCase
2+
from mat3ra.esse.models.software.application import ApplicationSchemaBase
3+
4+
5+
class Application(ApplicationSchemaBase, InMemoryEntitySnakeCase):
6+
"""
7+
Application class representing a software application.
8+
9+
Attributes:
10+
name: Application name (required)
11+
version: Application version
12+
build: Application build
13+
shortName: Short name of the application
14+
summary: Application's short description
15+
hasAdvancedComputeOptions: Whether advanced compute options are present
16+
isLicensed: Whether licensing is present
17+
isDefault: Identifies that entity is defaultable
18+
schemaVersion: Entity's schema version
19+
"""
20+
21+
@property
22+
def is_using_material(self) -> bool:
23+
material_using_applications = ["vasp", "nwchem", "espresso"]
24+
return self.name in material_using_applications
25+
26+
def get_short_name(self) -> str:
27+
return self.shortName if self.shortName else self.name
28+

src/py/mat3ra/ade/context/__init__.py

Whitespace-only changes.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from typing import Any, Dict, Optional
2+
3+
from mat3ra.code.entity import InMemoryEntitySnakeCase
4+
from mat3ra.esse.models.context_provider import ContextProviderSchema
5+
6+
7+
class ContextProvider(ContextProviderSchema, InMemoryEntitySnakeCase):
8+
"""
9+
Context provider for a template.
10+
11+
Attributes:
12+
name: The name of this item (required)
13+
domain: Domain of the context provider
14+
entityName: Entity name associated with the context provider
15+
data: Data object for the context provider
16+
extraData: Additional data object for the context provider
17+
isEdited: Flag indicating if the context provider has been edited
18+
context: Context object for the context provider
19+
"""
20+
21+
@property
22+
def name_str(self) -> str:
23+
return self.name.value if hasattr(self.name, 'value') else str(self.name)
24+
25+
@property
26+
def extra_data_key(self) -> str:
27+
return f"{self.name_str}ExtraData"
28+
29+
@property
30+
def is_edited_key(self) -> str:
31+
return f"is{self.name_str}Edited"
32+
33+
@property
34+
def is_unit_context_provider(self) -> bool:
35+
return self.entityName == "unit"
36+
37+
@property
38+
def is_subworkflow_context_provider(self) -> bool:
39+
return self.entityName == "subworkflow"
40+
41+
def _get_data_from_context(self, context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
42+
if not context:
43+
return {}
44+
data = context.get(self.name_str)
45+
is_edited = context.get(self.is_edited_key)
46+
extra_data = context.get(self.extra_data_key)
47+
result = {}
48+
if data is not None:
49+
result["data"] = data
50+
if is_edited is not None:
51+
result["isEdited"] = is_edited
52+
if extra_data is not None:
53+
result["extraData"] = extra_data
54+
return result
55+
56+
def _get_effective_data(self, context: Optional[Dict[str, Any]] = None) -> Any:
57+
context_data = self._get_data_from_context(context or self.context)
58+
return context_data.get("data", self.data)
59+
60+
def _get_effective_is_edited(self, context: Optional[Dict[str, Any]] = None) -> bool:
61+
context_data = self._get_data_from_context(context or self.context)
62+
return context_data.get("isEdited", self.isEdited)
63+
64+
def _get_effective_extra_data(self, context: Optional[Dict[str, Any]] = None) -> Optional[Any]:
65+
context_data = self._get_data_from_context(context or self.context)
66+
return context_data.get("extraData", self.extraData)
67+
68+
def yield_data(self, context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
69+
data = self._get_effective_data(context)
70+
is_edited = self._get_effective_is_edited(context)
71+
extra_data = self._get_effective_extra_data(context)
72+
result = {
73+
self.name_str: data,
74+
self.is_edited_key: is_edited,
75+
}
76+
if extra_data:
77+
result[self.extra_data_key] = extra_data
78+
return result
79+
80+
def yield_data_for_rendering(self, context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
81+
return self.yield_data(context)
82+
83+
def merge_context_data(self, result: Dict[str, Any], provider_context: Optional[Dict[str, Any]] = None) -> None:
84+
"""
85+
Merge this provider's rendering context data into result dictionary.
86+
Merges context keys if they are objects, otherwise overrides them.
87+
88+
Args:
89+
result: Dictionary to merge into (modified in place)
90+
provider_context: Optional external context to override provider's internal data
91+
"""
92+
context = self.yield_data_for_rendering(provider_context)
93+
for key, value in context.items():
94+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
95+
result[key] = {**result[key], **value}
96+
else:
97+
result[key] = value
98+
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from pydantic import Field
2+
3+
from .context_provider import ContextProvider
4+
5+
6+
class JinjaContextProvider(ContextProvider):
7+
is_using_jinja_variables: bool = Field(
8+
default=False, description="Whether this provider uses Jinja variables"
9+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from typing import Optional, Dict, Any
2+
3+
from pydantic import Field
4+
5+
from .jinja_context_provider import JinjaContextProvider
6+
7+
8+
class JSONSchemaDataProvider(JinjaContextProvider):
9+
json_schema: Optional[Dict[str, Any]] = Field(default=None, description="JSON schema for this provider")

src/py/mat3ra/ade/executable.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from mat3ra.code.entity import InMemoryEntitySnakeCase
2+
from mat3ra.esse.models.software.executable import ExecutableSchema
3+
4+
5+
class Executable(ExecutableSchema, InMemoryEntitySnakeCase):
6+
"""
7+
Executable class representing an executable of an application.
8+
9+
Attributes:
10+
name: The name of the executable (required)
11+
applicationId: IDs of the application this executable belongs to
12+
hasAdvancedComputeOptions: Whether advanced compute options are present
13+
isDefault: Identifies that entity is defaultable
14+
schemaVersion: Entity's schema version
15+
preProcessors: Names of the pre-processors for this calculation
16+
postProcessors: Names of the post-processors for this calculation
17+
monitors: Names of the monitors for this calculation
18+
results: Names of the results for this calculation
19+
"""
20+
21+
pass

0 commit comments

Comments
 (0)