Skip to content

Commit

Permalink
Merge pull request #100 from Exabyte-io/update/SOF-7321
Browse files Browse the repository at this point in the history
chore: add in_memory_entity
  • Loading branch information
timurbazhirov authored Apr 8, 2024
2 parents c3755c4 + 220058b commit 42b17b1
Show file tree
Hide file tree
Showing 13 changed files with 241 additions and 20 deletions.
1 change: 0 additions & 1 deletion .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,3 @@
"@babel/plugin-proposal-class-properties"
]
}

1 change: 0 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
{
"extends": ["@exabyte-io/eslint-config"]
}

2 changes: 1 addition & 1 deletion .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
path: actions

- name: Run python unit tests
uses: ./actions/py/test
uses: ./actions/py/pytest
with:
python-version: ${{ matrix.python-version }}
unit-test-directory: tests/py/unit
Expand Down
1 change: 0 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,3 @@
"trailingComma": "all",
"tabWidth": 4
}

1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,3 @@ npm run transpile
# run tests
npm run test
```

8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ classifiers = [
]
dependencies = [
# add requirements here
"numpy"
"numpy",
"jsonschema>=2.6.0",
"mat3ra-utils",
]

[project.optional-dependencies]
Expand All @@ -27,7 +29,9 @@ tests = [
"ruff",
"isort",
"mypy",
"pip-tools"
"pip-tools",
"pytest",
"pytest-cov",
]
all = ["mat3ra-code[tests]"]

Expand Down
33 changes: 30 additions & 3 deletions src/py/mat3ra/code/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
import numpy as np
from typing import Any, Dict, Optional

from mat3ra.utils import object as object_utils

def get_length(vec: np.ndarray) -> np.float_:
return np.linalg.norm(vec)

class BaseUnderscoreJsonPropsHandler(object):
def __init__(self, config: Dict[str, Any] = {}) -> None:
self._json = object_utils.clone_deep(config)

def __getattribute__(self, item):
try:
default_attribute = super().__getattribute__(item)
return default_attribute
except AttributeError:
return self.__getattribute_from_json__(item)

def __getattribute_from_json__(self, name: str, default_value: Any = None) -> Any:
return self._json.get(name, default_value)

def get_prop(self, name: str, default_value: Any = None) -> Any:
return self.__getattribute_from_json__(name, default_value)

def set_prop(self, name: str, value: Any) -> None:
object_utils.set(self._json, name, value)

def unset_prop(self, name: str) -> None:
del self._json[name]

def set_props(self, json: Dict[str, Any] = {}) -> "BaseUnderscoreJsonPropsHandler":
for key, value in json.items():
self.set_prop(key, value)
return self
25 changes: 25 additions & 0 deletions src/py/mat3ra/code/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class Coefficients:
EV_TO_RY = 0.0734986176
BOHR_TO_ANGSTROM = 0.52917721092
ANGSTROM_TO_BOHR = 1 / 0.52917721092
EV_A_TO_RY_BOHR = 1 / 25.71104309541616


class Tolerance:
# in crystal coordinates
length = 0.01
lengthAngstrom = 0.001
pointsDistance = 0.001


class Units:
bohr = "bohr"
angstrom = "angstrom"
degree = "degree"
radian = "radian"
alat = "alat"


class AtomicCoordinateUnits:
crystal = "crystal"
cartesian = "cartesian"
98 changes: 98 additions & 0 deletions src/py/mat3ra/code/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from typing import Any, Dict, List, Optional

import jsonschema
from mat3ra.utils import object as object_utils

from . import BaseUnderscoreJsonPropsHandler
from .mixins import DefaultableMixin, HasDescriptionMixin, HasMetadataMixin, NamedMixin


class ValidationErrorCode:
IN_MEMORY_ENTITY_DATA_INVALID = "IN_MEMORY_ENTITY_DATA_INVALID"


class ErrorDetails:
def __init__(self, error: Optional[Dict[str, Any]], json: Dict[str, Any], schema: Dict):
self.error = error
self.json = json
self.schema = schema


class EntityError(Exception):
def __init__(self, code: ValidationErrorCode, details: Optional[ErrorDetails] = None):
super().__init__(code)
self.code = code
self.details = details


class InMemoryEntity(BaseUnderscoreJsonPropsHandler):
jsonSchema: Optional[Dict] = None

@classmethod
def get_cls(cls) -> str:
return cls.__name__

@property
def cls(self) -> str:
return self.__class__.__name__

def get_cls_name(self) -> str:
return self.__class__.__name__

@classmethod
def create(cls, config: Dict[str, Any]) -> Any:
return cls(config)

def to_json(self, exclude: List[str] = []) -> Dict[str, Any]:
return self.clean(object_utils.clone_deep(object_utils.omit(self._json, exclude)))

def clone(self, extra_context: Dict[str, Any] = {}) -> Any:
return self.__class__.__init__({**self.to_json(), **extra_context})

@staticmethod
def validate_data(data: Dict[str, Any], clean: bool = False):
if clean:
print("Error: clean is not supported for InMemoryEntity.validateData")
if InMemoryEntity.jsonSchema:
jsonschema.validate(data, InMemoryEntity.jsonSchema)

def validate(self) -> None:
if self._json:
self.__class__.validate_data(self._json)

def clean(self, config: Dict[str, Any]) -> Dict[str, Any]:
# Not implemented, consider the below for the implementation
# https://stackoverflow.com/questions/44694835/remove-properties-from-json-object-not-present-in-schema
return config

def is_valid(self) -> bool:
try:
self.validate()
return True
except EntityError:
return False

# Properties
@property
def id(self) -> str:
return self.prop("_id", "")

@id.setter
def id(self, id: str) -> None:
self.set_prop("_id", id)

@property
def slug(self) -> str:
return self.prop("slug", "")

def get_as_entity_reference(self, by_id_only: bool = False) -> Dict[str, str]:
if by_id_only:
return {"_id": self.id}
else:
return {"_id": self.id, "slug": self.slug, "cls": self.get_cls_name()}


class HasDescriptionHasMetadataNamedDefaultableInMemoryEntity(
InMemoryEntity, DefaultableMixin, NamedMixin, HasMetadataMixin, HasDescriptionMixin
):
pass
57 changes: 57 additions & 0 deletions src/py/mat3ra/code/mixins/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from typing import Any, Dict

from .. import BaseUnderscoreJsonPropsHandler


class DefaultableMixin(BaseUnderscoreJsonPropsHandler):
__default_config__: Dict[str, Any]

@property
def is_default(self) -> bool:
return self.get_prop("isDefault", False)

@is_default.setter
def is_default(self, is_default: bool = False) -> None:
self.set_prop("isDefault", is_default)

@classmethod
def create_default(cls) -> "DefaultableMixin":
return cls(cls.__default_config__)


class NamedMixin(BaseUnderscoreJsonPropsHandler):
@property
def name(self) -> str:
return self.get_prop("name", False)

@name.setter
def name(self, name: str = "") -> None:
self.set_prop("name", name)


class HasMetadataMixin(BaseUnderscoreJsonPropsHandler):
@property
def metadata(self) -> Dict:
return self.get_prop("metadata", False)

@metadata.setter
def metadata(self, metadata: Dict = {}) -> None:
self.set_prop("metadata", metadata)


class HasDescriptionMixin(BaseUnderscoreJsonPropsHandler):
@property
def description(self) -> str:
return self.get_prop("description", "")

@description.setter
def description(self, description: str = "") -> None:
self.set_prop("description", description)

@property
def description_object(self) -> str:
return self.get_prop("descriptionObject", "")

@description_object.setter
def description_object(self, description_object: str = "") -> None:
self.set_prop("descriptionObject", description_object)
Empty file removed tests/py/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions tests/py/unit/test_entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from mat3ra.code.entity import InMemoryEntity

REFERENCE_OBJECT_1 = {"key1": "value1", "key2": "value2"}


def test_create():
in_memory_entity = InMemoryEntity.create({})
assert isinstance(in_memory_entity, InMemoryEntity)


def test_get_prop():
in_memory_entity = InMemoryEntity.create(REFERENCE_OBJECT_1)
assert in_memory_entity.get_prop("key1") == "value1"


def test_set_prop():
in_memory_entity = InMemoryEntity.create(REFERENCE_OBJECT_1)
in_memory_entity.set_prop("key3", "value3")
assert in_memory_entity.get_prop("key3") == "value3"


def test_to_json():
in_memory_entity = InMemoryEntity.create({})
assert in_memory_entity.to_json() == {}
10 changes: 0 additions & 10 deletions tests/py/unit/test_sample.py

This file was deleted.

0 comments on commit 42b17b1

Please sign in to comment.