Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experiments resource-based interface #106

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ matrix:
env: TOXENV=license
- python: 2.7
env: TOXENV=py27
- python: 3.4
env: TOXENV=py34
- python: 3.5
env: TOXENV=py35
- python: 3.6
Expand Down
13 changes: 13 additions & 0 deletions faculty/_util/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2018-2019 Faculty Science Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
99 changes: 99 additions & 0 deletions faculty/_util/resolvers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Copyright 2018-2019 Faculty Science Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


from uuid import UUID

from cachetools.func import lru_cache

from faculty.context import get_context
from faculty.clients import AccountClient, ProjectClient


def _make_uuid(value):
"""Make a UUID from the passed value.

Pass through UUID objects as the UUID constructor fails when passed UUID
objects.
"""
if isinstance(value, UUID):
return value
else:
return UUID(value)


def _project_from_name(session, name):
"""Provided a project name, find a matching project ID.

This method searches all the projects accessible to the active user for a
matching project. If not exactly one project matches, a ValueError is
raised.
"""

user_id = AccountClient(session).authenticated_user_id()
projects = ProjectClient(session).list_accessible_by_user(user_id)

matches = [project for project in projects if project.name == name]
if len(matches) == 1:
return matches[0]
elif len(matches) == 0:
raise ValueError("No projects of name {} found".format(name))
else:
raise ValueError("Multiple projects of name {} found".format(name))


@lru_cache()
def resolve_project_id(session, project=None):
"""Resolve the ID of a project based on ID, name or the current context.

This helper encapsulates logic for determining a project in three
situations:

* If ``None`` is passed as the project, or if no project is passed, the
project will be inferred from the runtime context (i.e. environment
variables), and so will correspond to the 'current project' when run
inside Faculty platform.
* If a ``uuid.UUID`` or a string containing a valid UUID is passed, this
will be assumed to be the ID of the project and will be returned.
* If any other string is passed, the Faculty platform will be queried for
projects matching that name. If exactly one of that name is accessible to
the user, its ID will be returned, otherwise a ``ValueError`` will be
raised.

Parameters
----------
session : faculty.session.Session
project : str, uuid.UUID or None
Information to use to determine the active project.

Returns
-------
uuid.UUID
The ID of the project
"""

if project is None:
context = get_context()
if context.project_id is None:
raise ValueError(
"Must pass a project name or ID when none can be determined "
"from the runtime context"
)
else:
return context.project_id
else:
try:
return _make_uuid(project)
except ValueError:
return _project_from_name(session, project).id
1 change: 1 addition & 0 deletions faculty/clients/experiment/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
RestoreExperimentRunsResponse,
RunIdFilter,
RunNumberSort,
SortOrder,
StartedAtSort,
Tag,
TagFilter,
Expand Down
81 changes: 73 additions & 8 deletions faculty/clients/experiment/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from collections import namedtuple
from enum import Enum

from attr import attrs, attrib


class LifecycleStage(Enum):
ACTIVE = "active"
Expand Down Expand Up @@ -81,21 +83,84 @@ class ComparisonOperator(Enum):
GREATER_THAN_OR_EQUAL_TO = "ge"


ProjectIdFilter = namedtuple("ProjectIdFilter", ["operator", "value"])
ExperimentIdFilter = namedtuple("ExperimentIdFilter", ["operator", "value"])
RunIdFilter = namedtuple("RunIdFilter", ["operator", "value"])
DeletedAtFilter = namedtuple("DeletedAtFilter", ["operator", "value"])
TagFilter = namedtuple("TagFilter", ["key", "operator", "value"])
ParamFilter = namedtuple("ParamFilter", ["key", "operator", "value"])
MetricFilter = namedtuple("MetricFilter", ["key", "operator", "value"])
def _matching_compound(filter, operator):
return isinstance(filter, CompoundFilter) and filter.operator == operator


def _combine_filters(first, second, op):
if _matching_compound(first, op) and _matching_compound(second, op):
conditions = first.conditions + second.conditions
elif _matching_compound(first, op):
conditions = first.conditions + [second]
elif _matching_compound(second, op):
conditions = [first] + second.conditions
else:
conditions = [first, second]
return CompoundFilter(op, conditions)


class BaseFilter(object):
def __and__(self, other):
return _combine_filters(self, other, LogicalOperator.AND)

def __or__(self, other):
return _combine_filters(self, other, LogicalOperator.OR)


@attrs
class ProjectIdFilter(BaseFilter):
operator = attrib()
value = attrib()


@attrs
class ExperimentIdFilter(BaseFilter):
operator = attrib()
value = attrib()


@attrs
class RunIdFilter(BaseFilter):
operator = attrib()
value = attrib()


@attrs
class DeletedAtFilter(BaseFilter):
operator = attrib()
value = attrib()


@attrs
class TagFilter(BaseFilter):
key = attrib()
operator = attrib()
value = attrib()


@attrs
class ParamFilter(BaseFilter):
key = attrib()
operator = attrib()
value = attrib()


@attrs
class MetricFilter(BaseFilter):
key = attrib()
operator = attrib()
value = attrib()


class LogicalOperator(Enum):
AND = "and"
OR = "or"


CompoundFilter = namedtuple("CompoundFilter", ["operator", "conditions"])
@attrs
class CompoundFilter(BaseFilter):
operator = attrib()
conditions = attrib()


class SortOrder(Enum):
Expand Down
Loading