Skip to content

Commit 2cf7b70

Browse files
authored
Improve serverless APIs + test coverage (#282)
1 parent 8252316 commit 2cf7b70

File tree

9 files changed

+1348
-101
lines changed

9 files changed

+1348
-101
lines changed

.github/workflows/ci_cd.yml

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
runs-on: ubuntu-latest
3131
steps:
3232
- name: PyAnsys code style checks
33-
uses: ansys/actions/code-style@v8
33+
uses: ansys/actions/code-style@v9.0
3434
with:
3535
python-version: ${{ env.MAIN_PYTHON_VERSION }}
3636
show-diff-on-failure: false
@@ -55,7 +55,7 @@ jobs:
5555
python-version: [ '3.10', '3.11', '3.12', '3.13' ]
5656
steps:
5757
- name: Build wheelhouse
58-
uses: ansys/actions/build-wheelhouse@v8
58+
uses: ansys/actions/build-wheelhouse@v9.0
5959
with:
6060
library-name: ${{ env.PACKAGE_NAME }}
6161
operating-system: ${{ matrix.os }}
@@ -75,7 +75,6 @@ jobs:
7575
matrix:
7676
os: [ ubuntu-latest ]
7777
python-version: [ '3.10', '3.11', '3.12', '3.13' ]
78-
7978
steps:
8079
- uses: actions/checkout@v4
8180

@@ -118,7 +117,7 @@ jobs:
118117
# needs: [docs-style]
119118
steps:
120119
- name: Run Ansys documentation building action
121-
uses: ansys/actions/doc-build@v8
120+
uses: ansys/actions/doc-build@v9.0
122121
with:
123122
python-version: ${{ env.MAIN_PYTHON_VERSION }}
124123
check-links: false
@@ -130,7 +129,7 @@ jobs:
130129
runs-on: ubuntu-latest
131130
steps:
132131
- name: Build library source and wheel artifacts
133-
uses: ansys/actions/build-library@v8
132+
uses: ansys/actions/build-library@v9.0
134133
with:
135134
library-name: ${{ env.PACKAGE_NAME }}
136135
python-version: ${{ env.MAIN_PYTHON_VERSION }}
@@ -142,14 +141,14 @@ jobs:
142141
runs-on: ubuntu-latest
143142
steps:
144143
- name: Release to the public PyPI repository
145-
uses: ansys/actions/release-pypi-public@v8
144+
uses: ansys/actions/release-pypi-public@v9.0
146145
with:
147146
library-name: ${{ env.PACKAGE_NAME }}
148147
twine-username: "__token__"
149148
twine-token: ${{ secrets.PYPI_TOKEN }}
150149

151150
- name: Release to GitHub
152-
uses: ansys/actions/release-github@v8
151+
uses: ansys/actions/release-github@v9.0
153152
if: ${{ !env.ACT }}
154153
with:
155154
library-name: ${{ env.PACKAGE_NAME }}
@@ -161,7 +160,7 @@ jobs:
161160
needs: [ docs, package ]
162161
steps:
163162
- name: Deploy the latest documentation
164-
uses: ansys/actions/doc-deploy-dev@v8
163+
uses: ansys/actions/doc-deploy-dev@v9.0
165164
if: ${{ !env.ACT }}
166165
with:
167166
cname: ${{ env.DOCUMENTATION_CNAME }}
@@ -176,7 +175,7 @@ jobs:
176175
needs: [ docs, release ]
177176
steps:
178177
- name: Deploy the stable documentation
179-
uses: ansys/actions/doc-deploy-stable@v8
178+
uses: ansys/actions/doc-deploy-stable@v9.0
180179
if: ${{ !env.ACT }}
181180
with:
182181
cname: ${{ env.DOCUMENTATION_CNAME }}
@@ -194,7 +193,7 @@ jobs:
194193
- name: Microsoft Teams Notification
195194
uses: jdcargile/[email protected]
196195
with:
197-
github-token: ${{ github.token }} # this will use the runner's token.
196+
github-token: ${{ github.token }}
198197
ms-teams-webhook-uri: ${{ secrets.MS_TEAMS_WEBHOOK_URI_CI }}
199198
notification-summary: CI build failure
200199
notification-color: dc3545

src/ansys/dynamicreporting/core/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,9 @@ class IntegrityError(ADRException):
135135
"""Exception raised if there is a constraint violation while saving an object in the database."""
136136

137137
detail = "A database integrity check failed."
138+
139+
140+
class InvalidFieldError(ADRException):
141+
"""Exception raised if a field is not valid."""
142+
143+
detail = "Field is invalid."

src/ansys/dynamicreporting/core/serverless/adr.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -818,10 +818,16 @@ def copy_objects(
818818
"'target_media_dir' argument must be specified because one of the objects"
819819
" contains media to copy.'"
820820
)
821-
# save or load sessions, datasets - since it is possible they are shared
821+
# try to load sessions, datasets - since it is possible they are shared
822822
# and were saved already.
823-
session, _ = Session.get_or_create(**item.session.as_dict(), using=target_database)
824-
dataset, _ = Dataset.get_or_create(**item.dataset.as_dict(), using=target_database)
823+
try:
824+
session = Session.get(guid=item.session.guid, using=target_database)
825+
except Session.DoesNotExist:
826+
session = Session.create(**item.session.as_dict(), using=target_database)
827+
try:
828+
dataset = Dataset.get(guid=item.dataset.guid, using=target_database)
829+
except Dataset.DoesNotExist:
830+
dataset = Dataset.create(**item.dataset.as_dict(), using=target_database)
825831
item.session = session
826832
item.dataset = dataset
827833
copy_list.append(item)

src/ansys/dynamicreporting/core/serverless/base.py

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
from django.db.utils import IntegrityError as DBIntegrityError
2424

2525
from ..exceptions import (
26-
ADRException,
2726
IntegrityError,
27+
InvalidFieldError,
2828
MultipleObjectsReturnedError,
2929
ObjectDoesNotExistError,
3030
ObjectNotSavedError,
@@ -44,7 +44,9 @@ def wrapper(*args, **kwargs):
4444
try:
4545
return func(*args, **kwargs)
4646
except (FieldError, FieldDoesNotExist, ValidationError, DataError) as e:
47-
raise ADRException(extra_detail=f"One or more fields set or accessed are invalid: {e}")
47+
raise InvalidFieldError(
48+
extra_detail=f"One or more fields set or accessed are invalid: {e}"
49+
)
4850

4951
return wrapper
5052

@@ -75,13 +77,11 @@ def __new__(
7577
if parents:
7678
# dynamically make the properties listed into class attrs
7779
if "_properties" in namespace:
78-
dynamic_props_field = namespace["_properties"]
79-
if hasattr(dynamic_props_field, "default"):
80-
props = dynamic_props_field.default
81-
new_namespace = {**namespace}
82-
for prop in props:
83-
new_namespace[prop] = None
84-
new_cls = super_new(mcs, cls_name, bases, new_namespace, **kwargs)
80+
props = namespace["_properties"]
81+
new_namespace = {**namespace}
82+
for prop in props:
83+
new_namespace[prop] = None
84+
new_cls = super_new(mcs, cls_name, bases, new_namespace, **kwargs)
8585
# save every class extending BaseModel
8686
mcs._cls_registry[cls_name] = new_cls
8787
# add exceptions
@@ -266,7 +266,7 @@ def _orm_db(self) -> str:
266266
def db(self):
267267
return self._orm_db
268268

269-
def as_dict(self):
269+
def as_dict(self, recursive=False) -> dict[str, Any]:
270270
out_dict = {}
271271
# use a combination of vars and fields
272272
cls_fields = set(self._get_field_names() + self._get_var_field_names())
@@ -276,6 +276,9 @@ def as_dict(self):
276276
value = getattr(self, field_, None)
277277
if value is None: # skip and use defaults
278278
continue
279+
if isinstance(value, list) and recursive:
280+
# convert to guids
281+
value = [obj.guid for obj in value]
279282
out_dict[field_] = value
280283
return out_dict
281284

@@ -293,7 +296,14 @@ def _prepare_for_save(self, **kwargs):
293296
continue
294297
if isinstance(value, list):
295298
objs = [obj._orm_instance for obj in value]
296-
getattr(self._orm_instance, field_).add(*objs)
299+
try:
300+
getattr(self._orm_instance, field_).add(*objs)
301+
except (ObjectDoesNotExist, ValueError) as e:
302+
if value:
303+
obj_cls = value[0].__class__
304+
raise obj_cls.NotSaved(extra_detail=str(e))
305+
else:
306+
raise ValueError(str(e))
297307
else:
298308
if isinstance(value, BaseModel): # relations
299309
try:
@@ -340,7 +350,7 @@ def delete(self, **kwargs):
340350
def from_db(cls, orm_instance, parent=None):
341351
cls_fields = dict(cls._get_field_names(with_types=True, include_private=True))
342352
model_fields = cls._get_orm_field_names(orm_instance)
343-
obj = cls()
353+
obj = cls.__new__(cls) # Bypass __init__ to skip validation
344354
for field_ in model_fields:
345355
if field_ in cls_fields:
346356
attr = field_
@@ -416,6 +426,11 @@ def create(cls, **kwargs):
416426
@classmethod
417427
@handle_field_errors
418428
def get(cls, **kwargs):
429+
"""Get an object from the database using the ORM model."""
430+
# convert basemodel instances to orm instances
431+
for key, value in kwargs.items():
432+
if isinstance(value, BaseModel):
433+
kwargs[key] = value._orm_instance
419434
try:
420435
orm_instance = cls._orm_model_cls.objects.using(kwargs.pop("using", "default")).get(
421436
**kwargs
@@ -429,39 +444,26 @@ def get(cls, **kwargs):
429444

430445
@classmethod
431446
@handle_field_errors
432-
def get_or_create(cls, **kwargs):
433-
try:
434-
return cls.get(**kwargs), False
435-
except cls.DoesNotExist:
436-
# Try to create an object using passed params.
437-
try:
438-
return cls.create(**kwargs), True
439-
except cls.IntegrityError:
440-
try:
441-
return cls.get(**kwargs), False
442-
except cls.DoesNotExist:
443-
pass
444-
raise
445-
446-
@classmethod
447-
@handle_field_errors
448-
def filter(cls, **kwargs):
447+
def filter(cls, **kwargs) -> "ObjectSet":
448+
for key, value in kwargs.items():
449+
if isinstance(value, BaseModel):
450+
kwargs[key] = value._orm_instance
449451
qs = cls._orm_model_cls.objects.using(kwargs.pop("using", "default")).filter(**kwargs)
450452
return ObjectSet(_model=cls, _orm_model=cls._orm_model_cls, _orm_queryset=qs)
451453

452454
@classmethod
453455
@handle_field_errors
454-
def find(cls, query="", **kwargs):
456+
def find(cls, query="", **kwargs) -> "ObjectSet":
455457
qs = cls._orm_model_cls.find(query=query, **kwargs)
456458
return ObjectSet(_model=cls, _orm_model=cls._orm_model_cls, _orm_queryset=qs)
457459

458-
def get_tags(self):
460+
def get_tags(self) -> str:
459461
return self.tags
460462

461-
def set_tags(self, tag_str):
463+
def set_tags(self, tag_str: str) -> None:
462464
self.tags = tag_str
463465

464-
def add_tag(self, tag, value=None):
466+
def add_tag(self, tag: str, value: str | None = None) -> None:
465467
self.rem_tag(tag)
466468
tags = shlex.split(self.get_tags())
467469
if value:
@@ -470,14 +472,14 @@ def add_tag(self, tag, value=None):
470472
tags.append(tag)
471473
self._rebuild_tags(tags)
472474

473-
def rem_tag(self, tag):
475+
def rem_tag(self, tag: str) -> None:
474476
tags = shlex.split(self.get_tags())
475477
for t in tags:
476478
if t == tag or t.split("=")[0] == tag:
477479
tags.remove(t)
478480
self._rebuild_tags(tags)
479481

480-
def remove_tag(self, tag):
482+
def remove_tag(self, tag: str) -> None:
481483
self.rem_tag(tag)
482484

483485

@@ -532,8 +534,8 @@ def delete(self):
532534

533535
def values_list(self, *fields, flat=False):
534536
if flat and len(fields) > 1:
535-
raise TypeError(
536-
"'flat' is not valid when values_list is called with more than one " "field."
537+
raise ValueError(
538+
"'flat' is not valid when values_list is called with more than one field."
537539
)
538540
ret = []
539541
for obj in self._obj_set:
@@ -555,11 +557,9 @@ def __get__(self, obj, obj_type=None):
555557
return getattr(obj, self._name, self._default)
556558

557559
def __set__(self, obj, value):
558-
cleaned_value = None
559-
if value is not None:
560-
cleaned_value = self.process(value, obj)
560+
cleaned_value = self.process(value, obj)
561561
setattr(obj, self._name, cleaned_value)
562562

563563
@abstractmethod
564564
def process(self, value, obj):
565-
pass
565+
pass # pragma: no cover

0 commit comments

Comments
 (0)