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

Implement feature to encode and retrieve signac elements by URI. #189

Draft
wants to merge 10 commits into
base: feature/integrated-queries
Choose a base branch
from
5 changes: 5 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ Highlights
- Support for compressed Collection files.


next
----

- Keep signac shell command history on a per-project basis.

[1.1.0] -- 2019-05-19
---------------------

Expand Down
2 changes: 2 additions & 0 deletions signac/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from .core.jsondict import JSONDict
from .core.h5store import H5Store
from .core.h5store import H5StoreManager
from .uri import open


__version__ = '1.1.0'
Expand All @@ -61,4 +62,5 @@
'buffered', 'is_buffered', 'flush', 'get_buffer_size', 'get_buffer_load',
'JSONDict',
'H5Store', 'H5StoreManager',
'open',
]
12 changes: 12 additions & 0 deletions signac/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
import logging
import getpass
import difflib
import atexit
import code
import importlib
import platform
from rlcompleter import Completer
import re
import errno
Expand Down Expand Up @@ -1000,6 +1002,16 @@ def jobs():
interpreter.runsource(args.command, filename="<input>", symbol="exec")
else: # interactive
if READLINE:
if 'PyPy' not in platform.python_implementation():
fn_hist = project.fn('.signac_shell_history')
try:
readline.read_history_file(fn_hist)
readline.set_history_length(1000)
except (IOError, OSError) as error:
if error.errno != errno.ENOENT:
raise
atexit.register(readline.write_history_file, fn_hist)

readline.set_completer(Completer(local_ns).complete)
readline.parse_and_bind('tab: complete')
code.interact(
Expand Down
129 changes: 111 additions & 18 deletions signac/contrib/filterparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
# This software is licensed under the BSD 3-Clause License.
from __future__ import print_function
import sys

from ..core import json
from ..common import six
Copy link
Collaborator

Choose a reason for hiding this comment

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

We can get rid of the py2/3 boiler plate

from ..common.six.moves.urllib.parse import urlencode, parse_qsl, quote_plus, unquote
if six.PY2:
from collections import Mapping, Iterable
else:
from collections.abc import Mapping, Iterable


def _print_err(msg=None):
Expand Down Expand Up @@ -60,28 +67,31 @@ def _cast(x):
print("Did you mean {}?".format(CAST_MAPPING_WARNING[x], file=sys.stderr))
return CAST_MAPPING[x]
except KeyError:
try:
return int(x)
except ValueError:
if x.startswith('"') and x.endswith('"'):
return x[1:-1]
else:
try:
return float(x)
return int(x)
except ValueError:
return x
try:
return float(x)
except ValueError:
return x


def _parse_simple(key, value=None):
if value is None or value == '!':
return {key: {'$exists': True}}
return key, {'$exists': True}
elif _is_json(value):
return {key: _parse_json(value)}
return key, _parse_json(value)
elif _is_regex(value):
return {key: {'$regex': value[1:-1]}}
return key, {'$regex': value[1:-1]}
elif _is_json(key):
raise ValueError(
"Please check your filter arguments. "
"Using as JSON expression as key is not allowed: '{}'.".format(key))
else:
return {key: _cast(value)}
return key, _cast(value)


def parse_filter_arg(args, file=sys.stderr):
Expand All @@ -91,14 +101,97 @@ def parse_filter_arg(args, file=sys.stderr):
if _is_json(args[0]):
return _parse_json(args[0])
else:
return _with_message(_parse_simple(args[0]), file)
key, value = _parse_simple(args[0])
return _with_message({key: value}, file)
else:
q = dict()
for i in range(0, len(args), 2):
key = args[i]
if i+1 < len(args):
value = args[i+1]
else:
value = None
q.update(_parse_simple(key, value))
q = dict(parse_simple(args))

return _with_message(q, file)


def parse_simple(tokens):
for i in range(0, len(tokens), 2):
key = tokens[i]
if i+1 < len(tokens):
value = tokens[i+1]
else:
value = None
yield _parse_simple(key, value)


def _add_prefix(filter, prefix):
for key, value in filter:
if key in ('$and', '$or'):
if isinstance(value, list) or isinstance(value, tuple):
yield key, [dict(_add_prefix(item.items(), prefix)) for item in value]
else:
raise ValueError(
"The argument to a logical operator must be a sequence (e.g. a list)!")
elif '.' in key and key.split('.', 1)[0] in ('sp', 'doc'):
yield key, value
elif key in ('sp', 'doc'):
yield key, value
else:
yield prefix + '.' + key, value


def _root_keys(filter):
for key, value in filter.items():
if key in ('$and', '$or'):
assert isinstance(value, (list, tuple))
for item in value:
for key in _root_keys(item):
yield key
elif '.' in key:
yield key.split('.', 1)[0]
else:
yield key


def _parse_filter(filter):
if isinstance(filter, six.string_types):
# yield from parse_simple(filter.split()) # TODO: After dropping Py27.
for key, value in parse_simple(filter.split()):
yield key, value
elif filter:
# yield from filter.items() # TODO: After dropping Py27.
for key, value in filter.items():
yield key, value


def parse_filter(filter, prefix='sp'):
# yield from _add_prefix(_parse_filter(filter), prefix) # TODO: After dropping Py27.
for key, value in _add_prefix(_parse_filter(filter), prefix):
yield key, value


def _parse_filter_query(query):
for key, value in dict(parse_qsl(query)).items():
yield key, _cast(unquote(value))


def _flatten(filter):
for key, value in filter.items():
if isinstance(value, Mapping):
for k, v in _flatten(value):
yield key + '.' + k, v
else:
yield key, value


def _urlencode_filter(filter):
for key, value in _flatten(filter):
if isinstance(value, six.string_types):
yield key, quote_plus('"' + value + '"')
elif isinstance(value, Iterable):
yield key, ','.join([_urlencode_filter(i) for i in value])
elif value is None:
yield key, 'null'
elif isinstance(value, bool):
yield key, {True: 'true', False: 'false'}[value]
else:
yield key, value


def urlencode_filter(filter):
return urlencode(list(_urlencode_filter(filter)))
2 changes: 1 addition & 1 deletion signac/contrib/import_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def _make_schema_based_path_function(jobs, exclude_keys=None, delimiter_nested='
if len(jobs) <= 1:
return lambda job: ''

index = [{'_id': job._id, 'statepoint': job.sp()} for job in jobs]
index = [{'_id': job._id, 'sp': job.sp()} for job in jobs]
jsi = _build_job_statepoint_index(jobs=jobs, exclude_const=True, index=index)
sp_index = OrderedDict(jsi)

Expand Down
3 changes: 3 additions & 0 deletions signac/contrib/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ def __repr__(self):
self.__class__.__module__ + '.' + self.__class__.__name__,
repr(self._project), self._statepoint)

def to_uri(self):
return '{}/api/v1/job/{}'.format(self._project.to_uri(), self.get_id())

def __eq__(self, other):
return hash(self) == hash(other)

Expand Down
4 changes: 2 additions & 2 deletions signac/contrib/linked_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ def create_linked_view(project, prefix=None, job_ids=None, index=None, path=None

if index is None:
if job_ids is None:
index = [{'_id': job._id, 'statepoint': job.sp()} for job in project]
index = [{'_id': job._id, 'sp': job.sp()} for job in project]
jobs = list(project)
else:
index = [{'_id': job_id, 'statepoint': project.open_job(id=job_id).sp()}
index = [{'_id': job_id, 'sp': project.open_job(id=job_id).sp()}
for job_id in job_ids]
jobs = list(project.open_job(id=job_id) for job_id in job_ids)
elif job_ids is not None:
Expand Down
Loading