[python-cligj] 01/05: Imported Upstream version 0.4.0

Sebastiaan Couwenberg sebastic at moszumanska.debian.org
Fri Dec 25 15:17:24 UTC 2015


This is an automated email from the git hooks/post-receive script.

sebastic pushed a commit to branch master
in repository python-cligj.

commit 84e0b63c710be60f544af07b5fb599929a42cb40
Author: Bas Couwenberg <sebastic at xs4all.nl>
Date:   Fri Dec 25 16:06:55 2015 +0100

    Imported Upstream version 0.4.0
---
 .travis.yml               |  15 ++++-
 CHANGES.txt               |   6 ++
 README.rst                | 141 +++++++++++++++++++++++++++++++---------------
 cligj/__init__.py         |  13 ++++-
 cligj/features.py         | 108 +++++++++++++++++++++++++++++++++++
 setup.py                  |   4 +-
 tests/onepoint.geojson    |   4 ++
 tests/test_features.py    | 126 +++++++++++++++++++++++++++++++++++++++++
 tests/twopoints.geojson   |   1 +
 tests/twopoints_seq.txt   |   2 +
 tests/twopoints_seqrs.txt |   7 +++
 11 files changed, 377 insertions(+), 50 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 7aa2ae3..121e475 100755
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,13 +1,22 @@
+sudo: false
 language: python
 python:
   - "2.7"
+  - "3.3"
   - "3.4"
+  - "3.5"
 install:
   - "pip install coveralls"
   - "pip install -e .[test]"
 script: 
-  - py.test
-  - coverage run --source=cligj -m py.test
+  - py.test --cov cligj --cov-report term-missing
 after_success:
   - coveralls
-sudo: false
+deploy:
+  on:
+    tags: true
+  provider: pypi
+  distributions: "sdist bdist_wheel"
+  user: seang
+  password:
+    secure: "dB3c7Ha9wYvdbpFn/FOb1sIDI0N/qvU5RKRFSLMsgp7rPuD0Vt4T8GMMWeiN+NmbgubAbe1sFhUUzXjh6Y7y/5Lolbd7lUTJLp4G+8v27ES6/9rVjMOZwoJFeRLOzF9Sl/ZONPo7zyI/fQS7x1BXfVaJKUhnSassyPABDU9dxw8="
diff --git a/CHANGES.txt b/CHANGES.txt
index defa767..0273e74 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,9 @@
+0.4.0 (2015-12-17)
+------------------
+- Introduces a click argument, `features_in_arg`, which utilizes a click
+  callback to normalize the input of geojson features (#9).
+- Release from tagged Travis CI builds (#10).
+
 0.3.0 (2015-08-12)
 ------------------
 - Deprecation of the cligj.plugins module (#6). Please switch to the 
diff --git a/README.rst b/README.rst
index 82475b8..2e22d44 100755
--- a/README.rst
+++ b/README.rst
@@ -9,6 +9,65 @@ cligj
 
 Common arguments and options for GeoJSON processing commands, using Click.
 
+`cligj` is for Python developers who create command line interfaces for geospatial data.
+`cligj` allows you to quickly build consistent, well-tested and interoperable CLIs for handling GeoJSON. 
+
+
+Arguments
+---------
+
+``files_in_arg``
+Multiple files
+
+``files_inout_arg``
+Multiple files, last of which is an output file.
+
+``features_in_arg``
+GeoJSON Features input which accepts multiple representations of GeoJSON features
+and returns the input data as an iterable of GeoJSON Feature-like dictionaries
+
+Options
+--------
+
+``verbose_opt``
+
+``quiet_opt``
+
+``format_opt``
+
+JSON formatting options
+~~~~~~~~~~~~~~~~~~~~~~~
+
+``indent_opt``
+
+``compact_opt``
+
+Coordinate precision option
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+``precision_opt``
+
+Geographic (default), projected, or Mercator switch
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+``projection_geographic_opt``
+
+``projection_projected_opt``
+
+``projection_mercator_opt``
+
+Feature collection or feature sequence switch
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+``sequence_opt``
+
+``use_rs_opt``
+
+GeoJSON output mode option
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+``geojson_type_collection_opt``
+
+``geojson_type_feature_opt``
+
+``def geojson_type_bbox_opt``
+
 Example
 -------
 
@@ -29,33 +88,62 @@ a delimiter, use the ``--rs`` option
     import cligj
     import json
 
+    def process_features(features):
+        for feature in features:
+            # TODO process feature here
+            yield feature
+
     @click.command()
+    @cligj.features_in_arg
     @cligj.sequence_opt
     @cligj.use_rs_opt
-    def features(sequence, use_rs):
-        features = [
-            {'type': 'Feature', 'id': '1'}, {'type': 'Feature', 'id': '2'}]
+    def pass_features(features, sequence, use_rs):
         if sequence:
-            for feature in features:
+            for feature in process_features(features):
                 if use_rs:
                     click.echo(b'\x1e', nl=False)
                 click.echo(json.dumps(feature))
         else:
             click.echo(json.dumps(
-                {'type': 'FeatureCollection', 'features': features}))
+                {'type': 'FeatureCollection',
+                 'features': list(process_features(features))}))
 
-On the command line it works like this.
+On the command line, the generated help text explains the usage
 
 .. code-block:: console
 
-    $ features
+    Usage: pass_features [OPTIONS] FEATURES...
+
+    Options:
+    --sequence / --no-sequence  Write a LF-delimited sequence of texts
+                                containing individual objects or write a single
+                                JSON text containing a feature collection object
+                                (the default).
+    --rs / --no-rs              Use RS (0x1E) as a prefix for individual texts
+                                in a sequence as per http://tools.ietf.org/html
+                                /draft-ietf-json-text-sequence-13 (default is
+                                False).
+    --help                      Show this message and exit.
+
+
+And can be used like this
+
+.. code-block:: console
+
+    $ cat data.geojson
+    {'type': 'FeatureCollection', 'features': [{'type': 'Feature', 'id': '1'}, {'type': 'Feature', 'id': '2'}]}
+
+    $ pass_features data.geojson
+    {'type': 'FeatureCollection', 'features': [{'type': 'Feature', 'id': '1'}, {'type': 'Feature', 'id': '2'}]}
+
+    $ cat data.geojson | pass_features
     {'type': 'FeatureCollection', 'features': [{'type': 'Feature', 'id': '1'}, {'type': 'Feature', 'id': '2'}]}
 
-    $ features --sequence
+    $ cat data.geojson | pass_features --sequence
     {'type': 'Feature', 'id': '1'}
     {'type': 'Feature', 'id': '2'}
 
-    $ features --sequence --rs
+    $ cat data.geojson | pass_features --sequence --rs
     ^^{'type': 'Feature', 'id': '1'}
     ^^{'type': 'Feature', 'id': '2'}
 
@@ -69,38 +157,3 @@ Plugins
    The cligj.plugins module is deprecated and will be removed at version 1.0.
    Use `click-plugins <https://github.com/click-contrib/click-plugins>`_
    instead.
-
-``cligj`` can also facilitate loading external `click-based
-<http://click.pocoo.org/4/>`_ plugins via `setuptools entry points
-<https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins>`_.
-The ``cligj.plugins`` module contains a special ``group()`` decorator that
-behaves exactly like ``click.group()`` except that it offers the opportunity
-load plugins and attach them to the group as it is istantiated.
-
-.. code-block:: python
-
-    from pkg_resources import iter_entry_points
-
-    import cligj.plugins
-    import click
-
-    @cligj.plugins.group(plugins=iter_entry_points('module.entry_points'))
-    def cli():
-
-        """A CLI application."""
-
-        pass
-
-    @cli.command()
-    @click.argument('arg')
-    def printer(arg):
-
-        """Print arg."""
-
-        click.echo(arg)
-
-    @cli.group(plugins=iter_entry_points('other_module.more_plugins'))
-    def plugins():
-
-        """A sub-group that contains plugins from a different module."""
-        pass
diff --git a/cligj/__init__.py b/cligj/__init__.py
index 7b2873e..aa4ef97 100755
--- a/cligj/__init__.py
+++ b/cligj/__init__.py
@@ -4,6 +4,8 @@
 
 import click
 
+from .features import normalize_feature_inputs
+
 # Arguments.
 
 # Multiple input files.
@@ -22,8 +24,17 @@ files_inout_arg = click.argument(
     required=True,
     metavar="INPUTS... OUTPUT")
 
-# Options.
+# Features input
+# Accepts multiple representations of GeoJSON features
+# Returns the input data as an iterable of GeoJSON Feature-like dictionaries
+features_in_arg = click.argument(
+    'features',
+    nargs=-1,
+    callback=normalize_feature_inputs,
+    metavar="FEATURES...")
 
+
+# Options.
 verbose_opt = click.option(
     '--verbose', '-v',
     count=True,
diff --git a/cligj/features.py b/cligj/features.py
new file mode 100644
index 0000000..97bb553
--- /dev/null
+++ b/cligj/features.py
@@ -0,0 +1,108 @@
+from itertools import chain
+import json
+import re
+
+import click
+
+
+def normalize_feature_inputs(ctx, param, features_like):
+    """ Click callback which accepts the following values:
+    * Path to file(s), each containing single FeatureCollection or Feature
+    * Coordinate pair(s) of the form "[0, 0]" or "0, 0" or "0 0"
+    * if not specified or '-', process STDIN stream containing
+        - line-delimited features
+        - ASCII Record Separator (0x1e) delimited features
+        - FeatureCollection or Feature object
+    and yields GeoJSON Features.
+    """
+    if len(features_like) == 0:
+        features_like = ('-',)
+
+    for flike in features_like:
+        try:
+            # It's a file/stream with GeoJSON
+            src = iter(click.open_file(flike, mode='r'))
+            for feature in iter_features(src):
+                yield feature
+        except IOError:
+            # It's a coordinate string
+            coords = list(coords_from_query(flike))
+            feature = {
+                'type': 'Feature',
+                'properties': {},
+                'geometry': {
+                    'type': 'Point',
+                    'coordinates': coords}}
+            yield feature
+
+
+def iter_features(src):
+    """Yield features from a src that may be either a GeoJSON feature
+    text sequence or GeoJSON feature collection."""
+    first_line = next(src)
+    # If input is RS-delimited JSON sequence.
+    if first_line.startswith(u'\x1e'):
+        buffer = first_line.strip(u'\x1e')
+        for line in src:
+            if line.startswith(u'\x1e'):
+                if buffer:
+                    feat = json.loads(buffer)
+                    yield feat
+                buffer = line.strip(u'\x1e')
+            else:
+                buffer += line
+        else:
+            feat = json.loads(buffer)
+            yield feat
+    else:
+        try:
+            feat = json.loads(first_line)
+            assert feat['type'] == 'Feature'
+            yield feat
+            for line in src:
+                feat = json.loads(line)
+                yield feat
+        except (TypeError, KeyError, AssertionError, ValueError):
+            text = "".join(chain([first_line], src))
+            feats = json.loads(text)
+            if feats['type'] == 'Feature':
+                yield feats
+            elif feats['type'] == 'FeatureCollection':
+                for feat in feats['features']:
+                    yield feat
+
+def iter_query(query):
+    """Accept a filename, stream, or string.
+    Returns an iterator over lines of the query."""
+    try:
+        itr = click.open_file(query).readlines()
+    except IOError:
+        itr = [query]
+    return itr
+
+
+def coords_from_query(query):
+    """Transform a query line into a (lng, lat) pair of coordinates."""
+    try:
+        coords = json.loads(query)
+    except ValueError:
+        vals = re.split(r"\,*\s*", query.strip())
+        coords = [float(v) for v in vals]
+    return tuple(coords[:2])
+
+
+def normalize_feature_objects(feature_objs):
+    """Takes an iterable of GeoJSON-like Feature mappings or
+    an iterable of objects with a geo interface and
+    normalizes it to the former."""
+    for obj in feature_objs:
+        if hasattr(obj, "__geo_interface__") and \
+           'type' in obj.__geo_interface__.keys() and \
+           obj.__geo_interface__['type'] == 'Feature':
+            yield obj.__geo_interface__
+        elif isinstance(obj, dict) and 'type' in obj and \
+                obj['type'] == 'Feature':
+            yield obj
+        else:
+            raise ValueError("Did not recognize object {0}"
+                             "as GeoJSON Feature".format(obj))
diff --git a/setup.py b/setup.py
index f09de36..34e7e00 100755
--- a/setup.py
+++ b/setup.py
@@ -8,7 +8,7 @@ with codecs_open('README.rst', encoding='utf-8') as f:
 
 
 setup(name='cligj',
-      version='0.3.0',
+      version='0.4.0',
       description=u"Click params for commmand line interfaces to GeoJSON",
       long_description=long_description,
       classifiers=[],
@@ -24,5 +24,5 @@ setup(name='cligj',
           'click>=4.0'
       ],
       extras_require={
-          'test': ['pytest'],
+          'test': ['pytest-cov'],
       })
diff --git a/tests/onepoint.geojson b/tests/onepoint.geojson
new file mode 100644
index 0000000..2323266
--- /dev/null
+++ b/tests/onepoint.geojson
@@ -0,0 +1,4 @@
+{"geometry": {"coordinates": [-122.7282, 45.5801], "type": "Point"},
+"id": "0",
+"properties": {},
+"type": "Feature"}
diff --git a/tests/test_features.py b/tests/test_features.py
new file mode 100644
index 0000000..3ccc76f
--- /dev/null
+++ b/tests/test_features.py
@@ -0,0 +1,126 @@
+import json
+import sys
+
+import pytest
+
+from cligj.features import \
+    coords_from_query, iter_query, \
+    normalize_feature_inputs, normalize_feature_objects
+
+
+def test_iter_query_string():
+    assert iter_query("lolwut") == ["lolwut"]
+
+
+def test_iter_query_file(tmpdir):
+    filename = str(tmpdir.join('test.txt'))
+    with open(filename, 'w') as f:
+        f.write("lolwut")
+    assert iter_query(filename) == ["lolwut"]
+
+
+def test_coords_from_query_json():
+    assert coords_from_query("[-100, 40]") == (-100, 40)
+
+
+def test_coords_from_query_csv():
+    assert coords_from_query("-100, 40") == (-100, 40)
+
+
+def test_coords_from_query_ws():
+    assert coords_from_query("-100 40") == (-100, 40)
+
+
+ at pytest.fixture
+def expected_features():
+    with open("tests/twopoints.geojson") as src:
+        fc = json.loads(src.read())
+        return fc['features']
+
+
+def _geoms(features):
+    geoms = []
+    for feature in features:
+        geoms.append(feature['geometry'])
+    return geoms
+
+
+def test_featurecollection_file(expected_features):
+    features = normalize_feature_inputs(None, 'features', ["tests/twopoints.geojson"])
+    assert _geoms(features) == _geoms(expected_features)
+
+
+def test_featurecollection_stdin(expected_features):
+    sys.stdin = open("tests/twopoints.geojson", 'r')
+    features = normalize_feature_inputs(None, 'features', [])
+    assert _geoms(features) == _geoms(expected_features)
+
+
+def test_featuresequence(expected_features):
+    features = normalize_feature_inputs(None, 'features', ["tests/twopoints_seq.txt"])
+    assert _geoms(features) == _geoms(expected_features)
+
+# TODO test path to sequence files fail
+
+def test_featuresequence_stdin(expected_features):
+    sys.stdin = open("tests/twopoints_seq.txt", 'r')
+    features = normalize_feature_inputs(None, 'features', [])
+    assert _geoms(features) == _geoms(expected_features)
+
+
+def test_singlefeature(expected_features):
+    features = normalize_feature_inputs(None, 'features', ["tests/onepoint.geojson"])
+    assert _geoms(features) == _geoms([expected_features[0]])
+
+
+def test_singlefeature_stdin(expected_features):
+    sys.stdin = open("tests/onepoint.geojson", 'r')
+    features = normalize_feature_inputs(None, 'features', [])
+    assert _geoms(features) == _geoms([expected_features[0]])
+
+
+def test_featuresequencers(expected_features):
+    features = normalize_feature_inputs(None, 'features', ["tests/twopoints_seqrs.txt"])
+    assert _geoms(features) == _geoms(expected_features)
+
+
+def test_featuresequencers_stdin(expected_features):
+    sys.stdin = open("tests/twopoints_seqrs.txt", 'r')
+    features = normalize_feature_inputs(None, 'features', [])
+    assert _geoms(features) == _geoms(expected_features)
+
+
+def test_coordarrays(expected_features):
+    inputs = ["[-122.7282, 45.5801]", "[-121.3153, 44.0582]"]
+    features = normalize_feature_inputs(None, 'features', inputs)
+    assert _geoms(features) == _geoms(expected_features)
+
+
+def test_coordpairs_comma(expected_features):
+    inputs = ["-122.7282, 45.5801", "-121.3153, 44.0582"]
+    features = normalize_feature_inputs(None, 'features', inputs)
+    assert _geoms(features) == _geoms(expected_features)
+
+
+def test_coordpairs_space(expected_features):
+    inputs = ["-122.7282 45.5801", "-121.3153 44.0582"]
+    features = normalize_feature_inputs(None, 'features', inputs)
+    assert _geoms(features) == _geoms(expected_features)
+
+
+class MockGeo(object):
+    def __init__(self, feature):
+        self.__geo_interface__ = feature
+
+
+def test_normalize_feature_objects(expected_features):
+    objs = [MockGeo(f) for f in expected_features]
+    assert expected_features == list(normalize_feature_objects(objs))
+    assert expected_features == list(normalize_feature_objects(expected_features))
+
+
+def test_normalize_feature_objects_bad(expected_features):
+    objs = [MockGeo(f) for f in expected_features]
+    objs.append(MockGeo(dict()))
+    with pytest.raises(ValueError):
+        list(normalize_feature_objects(objs))
diff --git a/tests/twopoints.geojson b/tests/twopoints.geojson
new file mode 100644
index 0000000..70fb618
--- /dev/null
+++ b/tests/twopoints.geojson
@@ -0,0 +1 @@
+{"features": [{"bbox": [-122.9292140099711, 45.37948199034149, -122.44106199104115, 45.858097009742835], "center": [-122.7282, 45.5801], "context": [{"id": "postcode.2503633822", "text": "97203"}, {"id": "region.3470299826", "text": "Oregon"}, {"id": "country.4150104525", "short_code": "us", "text": "United States"}], "geometry": {"coordinates": [-122.7282, 45.5801], "type": "Point"}, "id": "place.42767", "place_name": "Portland, Oregon, United States", "properties": {}, "relevance": 0.9 [...]
diff --git a/tests/twopoints_seq.txt b/tests/twopoints_seq.txt
new file mode 100644
index 0000000..9fa84aa
--- /dev/null
+++ b/tests/twopoints_seq.txt
@@ -0,0 +1,2 @@
+{"geometry": {"coordinates": [-122.7282, 45.5801], "type": "Point"}, "id": "0", "properties": {}, "type": "Feature"}
+{"geometry": {"coordinates": [-121.3153, 44.0582], "type": "Point"}, "id": "1", "properties": {}, "type": "Feature"}
diff --git a/tests/twopoints_seqrs.txt b/tests/twopoints_seqrs.txt
new file mode 100644
index 0000000..2670e39
--- /dev/null
+++ b/tests/twopoints_seqrs.txt
@@ -0,0 +1,7 @@
+
{"geometry": {"coordinates": [-122.7282, 45.5801], "type": "Point"}, "id": "0", "properties": {}, "type": "Feature"}
+
{"geometry": { "coordinates":
+    [-121.3153, 44.0582],
+    "type": "Point"},
+   "id": "1",
+   "properties": {},
+   "type": "Feature"}

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/python-cligj.git



More information about the Pkg-grass-devel mailing list