Skip to content
Closed
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