[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