Skip to content

Major refactor of the taurus <-> citrine-python interaction#223

Merged
maxhutch merged 17 commits intodevelopfrom
bugfix/flatten-fails-on-resource-redux
Feb 19, 2020
Merged

Major refactor of the taurus <-> citrine-python interaction#223
maxhutch merged 17 commits intodevelopfrom
bugfix/flatten-fails-on-resource-redux

Conversation

@maxhutch
Copy link
Copy Markdown
Contributor

@maxhutch maxhutch commented Feb 18, 2020

Citrine Python PR

Description

This PR is a major refactor of the way that citrine-python uses taurus. In taurus 0.6.0, the json serializer/deserializer was encapsulated in the TaurusJson() class. This allowed for the register_classes method to be exposed, which allows the class dictionary of a TaurusJson object to be updated. This change uses register_classes to override the deserialization of taurus objects like MaterialRun and ProcessTemplate to their citrine-python resource counterparts. That way, the taurus loads/dumps methods can be used to reconstruct material histories from the response of the material history route, which significantly simplifies the logic contained here in this repo (-923 lines!).

This leads to two other semi-significant changes:

  • The citrine.attributes no longer need to be duplicated from taurus; the taurus versions will do fine on their own
  • Storable was therefore rolled back into DataConcepts and the logic to include audit_info was moved there.

There are some consequences to the json format due to the upgrade from taurus 0.4.1 to 0.6.0, which are communicated in v0.5.0 release notes and v0.6.0 release notes.

PR Type:

  • Breaking change (fix or feature that would cause existing functionality to change)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

Adherence to team decisions

  • I have added tests for 100% coverage
  • I have written Numpy-style docstrings for every method and class.
  • I have communicated the downstream consequences of the PR to others.

@maxhutch maxhutch marked this pull request as ready for review February 19, 2020 03:07

encoder = None

def __init__(self, typ: str):
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

audit_info was moved here from Storable

Comment thread src/citrine/resources/data_concepts.py Outdated
@classmethod
def from_dict(cls, d: dict):
"""Build a data concepts option from a dictionary."""
audit_info = d.pop("audit_info", None)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a general pattern for if we want to add fields to taurus objects

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about having something like

citrine_python_fields = {'audit_info': AuditInfo}

at the class level and looping over that in this method?

Comment thread src/citrine/resources/data_concepts.py Outdated
setattr(dc_linked_obj, reverse_field, dc_obj)
# Re-establish the link between linked_obj and obj_with_soft_links
setattr(linked_obj, reverse_field, obj_with_soft_links)
return cls.get_encoder().copy(data)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So easy!

DataConcepts._make_class_dict()
cls.encoder = TaurusJson()
cls.encoder.register_classes(
{k: v for k, v in DataConcepts.class_dict.items() if k != "link_by_uid"}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where we override the taurus objects with their citrine-python equivalents

return MaterialRun.build(model)

# Add the root to the context and sort by writable order
blob = dict()
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reconstructs the json serialization format so that json deserialization can be used to build the connected material history

from taurus.util import flatten


def test_flatten():
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the originating test that we wanted to get passing.

@maxhutch maxhutch changed the title Major refactor of the taurus <-> citrine-python interaction (WIP) Major refactor of the taurus <-> citrine-python interaction Feb 19, 2020
@maxhutch
Copy link
Copy Markdown
Contributor Author

maxhutch commented Feb 19, 2020

Note that this will require https://github.com/CitrineInformatics/nextgen-devkit/pull/302

kroenlein
kroenlein previously approved these changes Feb 19, 2020
Copy link
Copy Markdown
Collaborator

@kroenlein kroenlein left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally looks good.

Comment thread requirements.txt Outdated
@@ -1,4 +1,4 @@
taurus-citrine==0.4.1
git+https://github.com/CitrineInformatics/taurus.git@develop
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the official requirements file pulling in develop?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's outdated; it was just for testing before the taurus 0.6.0 release.

Comment thread tests/resources/test_data_concepts.py
@maxhutch
Copy link
Copy Markdown
Contributor Author

I'm wondering if we shouldn't leave in citrine.attributes as deprecated aliases.

Comment thread src/citrine/resources/data_concepts.py Outdated
:py:mod:`JSON encoder <taurus.jsonr>`. The ensuing dictionary must
have a `type` field that corresponds to the response key of this class or of
:py:class:`LinkByUID <taurus.entity.link_by_uid.LinkByUID>`.
session: Session
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can remove

kroenlein
kroenlein previously approved these changes Feb 19, 2020
Copy link
Copy Markdown
Collaborator

@kroenlein kroenlein left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally agree. There's a residual

from taurus.client.json_encoder import loads, dumps

in tests/serialization/test_attribute_template.py that does not appear to be intentional. Should it be scrubbed?

asantas93
asantas93 previously approved these changes Feb 19, 2020
Copy link
Copy Markdown
Contributor

@asantas93 asantas93 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huge fan of this. Left a few small comments.

Comment thread src/citrine/resources/data_concepts.py Outdated
The name of the field
audit_info = d.pop("audit_info", None)
obj = super().from_dict(d)
obj.skip.add("_audit_info")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little put off by mutations to skip -- isn't it a class variable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we just don't need this.

Comment thread src/citrine/resources/data_concepts.py Outdated
@classmethod
def from_dict(cls, d: dict):
"""Build a data concepts option from a dictionary."""
audit_info = d.pop("audit_info", None)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about having something like

citrine_python_fields = {'audit_info': AuditInfo}

at the class level and looping over that in this method?

Comment thread src/citrine/resources/material_run.py Outdated
data['context'] + [data['root']],
key=lambda x: writable_sort_order(x["type"])
)
root_uid = next(iter(data['root']['uids'].items()))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
root_uid = next(iter(data['root']['uids'].items()))
root_scope, root_id = next(iter(data['root']['uids'].items()))

maybe?

Comment on lines +132 to +135
blob["context"] = sorted(
data['context'] + [data['root']],
key=lambda x: writable_sort_order(x["type"])
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

Comment thread src/citrine/resources/material_run.py Outdated

# Serialize using normal json (with the TaurusEncoder) and then deserialize with the
# TaurusJson encoder in order to rebuild the material history
return MaterialRun.get_encoder().loads(json.dumps(blob, cls=TaurusEncoder, sort_keys=True))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to move from python primitive types to citrine-python types directly? In my dream world git grep 'loads.*dumps' exits with failure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'd want to dive into get_resource to deserialize using get_json_support the first time rather than going round-robin through the string again. Let's leave that out of scope for this PR, though? It'd be an easy non-breaking change to make in the future.

@maxhutch maxhutch dismissed stale reviews from asantas93 and kroenlein via 5cd77ff February 19, 2020 15:53
if popped[field] is None:
setattr(obj, "_{}".format(field), None)
elif isinstance(popped[field], dict):
setattr(obj, "_{}".format(field), clazz.build(popped[field]))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we probably want to fall back on clazz(popped[field]) when hasattr(clazz, 'build') is false. That would allow us to use primitive types.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually there's probably a neater way than hasattr. Either way I think we can probably use that and ditch the is instance of dict check above.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issubclass seems to be the thing I'm thinking of instead of hasattr

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we cross that bridge when we get there? We'll have more information when we next decide to add a client_specific_field.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

Copy link
Copy Markdown

@bfolie bfolie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

@maxhutch maxhutch merged commit 5d64872 into develop Feb 19, 2020
@maxhutch maxhutch deleted the bugfix/flatten-fails-on-resource-redux branch February 19, 2020 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants