def test_dict_expected(self):
     data = {'a': [{'b': []}], 'd': 'e'}
     with pytest.raises(DataError) as de:
         update_merge_string_key(data, 'a__0', ['b'])
     assert de.value.error == 'Expected dict for a__0'
Exemplo n.º 2
0
def package_revise(context, data_dict):
    '''Revise a dataset (package) selectively with match, filter and
    update parameters.

    You must be authorized to edit the dataset and the groups that it belongs
    to.

    :param match: a dict containing "id" or "name" values of the dataset
                  to update, all values provided must match the current
                  dataset values or a ValidationError will be raised. e.g.
                  ``{"name": "my-data", "resources": {["name": "big.csv"]}}``
                  would abort if the my-data dataset's first resource name
                  is not "big.csv".
    :type match: dict
    :param filter: a list of string patterns of fields to remove from the
                   current dataset. e.g. ``"-resources__1"`` would remove the
                   second resource, ``"+title, +resources, -*"`` would
                   remove all fields at the dataset level except title and
                   all resources (default: ``[]``)
    :type filter: comma-separated string patterns or list of string patterns
    :param update: a dict with values to update/create after filtering
                   e.g. ``{"resources": [{"description": "file here"}]}`` would
                   update the description for the first resource
    :type update: dict
    :param include: a list of string pattern of fields to include in response
                    e.g. ``"-*"`` to return nothing (default: ``[]`` all
                    fields returned)
    :type include: comma-separated-string patterns or list of string patterns

    ``match`` and ``update`` parameters may also be passed as "flattened keys", using
    either the item numeric index or its unique id (with a minimum of 5 characters), e.g.
    ``update__resource__1f9ab__description="guidebook"`` would set the
    description of the resource with id starting with "1f9ab" to "guidebook", and
    ``update__resource__-1__description="guidebook"`` would do the same
    on the last resource in the dataset.

    The ``extend`` suffix can also be used on the update parameter to add
    a new item to a list, e.g. ``update__resources__extend=[{"name": "new resource", "url": "https://example.com"}]``
    will add a new resource to the dataset with the provided ``name`` and ``url``.

    Usage examples:

    * Change description in dataset, checking for old description::

        match={"notes": "old notes", "name": "xyz"}
        update={"notes": "new notes"}

    * Identical to above, but using flattened keys::

        match__name="xyz"
        match__notes="old notes"
        update__notes="new notes"

    * Replace all fields at dataset level only, keep resources (note: dataset id
      and type fields can't be deleted) ::

        match={"id": "1234abc-1420-cbad-1922"}
        filter=["+resources", "-*"]
        update={"name": "fresh-start", "title": "Fresh Start"}

    * Add a new resource (__extend on flattened key)::

        match={"id": "abc0123-1420-cbad-1922"}
        update__resources__extend=[{"name": "new resource", "url": "http://example.com"}]

    * Update a resource by its index::

        match={"name": "my-data"}
        update__resources__0={"name": "new name, first resource"}

    * Update a resource by its id (provide at least 5 characters)::

        match={"name": "their-data"}
        update__resources__19cfad={"description": "right one for sure"}

    * Replace all fields of a resource::

        match={"id": "34a12bc-1420-cbad-1922"}
        filter=["+resources__1492a__id", "-resources__1492a__*"]
        update__resources__1492a={"name": "edits here", "url": "http://example.com"}


    :returns: a dict containing 'package':the updated dataset with fields filtered by include parameter
    :rtype: dictionary

    '''
    model = context['model']

    schema = schema_.package_revise_schema()
    data, errors = _validate(data_dict, schema, context)
    if errors:
        model.Session.rollback()
        raise ValidationError(errors)

    name_or_id = (
        data['match__'].get('id') or
        data.get('match', {}).get('id') or
        data['match__'].get('name') or
        data.get('match', {}).get('name'))
    if name_or_id is None:
        raise ValidationError({'match__id': _('Missing value')})

    package_show_context = dict(
        context,
        for_update=True)
    orig = _get_action('package_show')(
        package_show_context,
        {'id': name_or_id})

    pkg = package_show_context['package']  # side-effect of package_show

    unmatched = []
    if 'match' in data:
        unmatched.extend(dfunc.check_dict(orig, data['match']))

    for k, v in sorted(data['match__'].items()):
        unmatched.extend(dfunc.check_string_key(orig, k, v))

    if unmatched:
        model.Session.rollback()
        raise ValidationError([{'match': [
            '__'.join(str(p) for p in unm)
            for unm in unmatched
        ]}])

    if 'filter' in data:
        orig_id = orig['id']
        dfunc.filter_glob_match(orig, data['filter'])
        orig['id'] = orig_id

    if 'update' in data:
        try:
            dfunc.update_merge_dict(orig, data['update'])
        except dfunc.DataError as de:
            model.Session.rollback()
            raise ValidationError([{'update': [de.error]}])

    # update __extend keys before __#__* so that files may be
    # attached to newly added resources in the same call
    try:
        for k, v in sorted(
                data['update__'].items(),
                key=lambda s: s[0][-6] if s[0].endswith('extend') else s[0]):
            dfunc.update_merge_string_key(orig, k, v)
    except dfunc.DataError as de:
        model.Session.rollback()
        raise ValidationError([{k: [de.error]}])

    _check_access('package_revise', context, {"update": orig})

    # future-proof return dict by putting package data under
    # "package". We will want to return activity info
    # on update or "nothing changed" status once possible
    rval = {
        'package': _get_action('package_update')(
            dict(context, package=pkg),
            orig)}
    if 'include' in data_dict:
        dfunc.filter_glob_match(rval, data_dict['include'])
    return rval
 def test_replace_parent(self):
     data = {'a': [{'b': 'c'}], 'd': 'e'}
     update_merge_string_key(data, 'd', 'd')
     assert data == {'a': [{'b': 'c'}], 'd': 'd'}
 def test_simple_list(self):
     data = {'a': ['q', 'w', 'e', 'r', 't']}
     update_merge_string_key(data, 'a__0', 'z')
     assert data == {'a': ['z', 'w', 'e', 'r', 't']}
 def test_replace_child(self):
     data = {'a': [{'b': 'c'}], 'd': 'e'}
     update_merge_string_key(data, 'a__0__b', 'z')
     assert data == {'a': [{'b': 'z'}], 'd': 'e'}
Exemplo n.º 6
0
 def test_dict_expected(self):
     data = {"a": [{"b": []}], "d": "e"}
     with pytest.raises(DataError) as de:
         update_merge_string_key(data, "a__0", ["b"])
     assert de.value.error == "Expected dict for a__0"
Exemplo n.º 7
0
 def test_simple_list(self):
     data = {"a": ["q", "w", "e", "r", "t"]}
     update_merge_string_key(data, "a__0", "z")
     assert data == {"a": ["z", "w", "e", "r", "t"]}
Exemplo n.º 8
0
 def test_replace_parent(self):
     data = {"a": [{"b": "c"}], "d": "e"}
     update_merge_string_key(data, "d", "d")
     assert data == {"a": [{"b": "c"}], "d": "d"}
Exemplo n.º 9
0
 def test_replace_child(self):
     data = {"a": [{"b": "c"}], "d": "e"}
     update_merge_string_key(data, "a__0__b", "z")
     assert data == {"a": [{"b": "z"}], "d": "e"}