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

features input #9

Merged
merged 7 commits into from
Dec 17, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
141 changes: 97 additions & 44 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------

Expand All @@ -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'}

Expand All @@ -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
13 changes: 12 additions & 1 deletion cligj/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import click

from .features import normalize_feature_inputs

# Arguments.

# Multiple input files.
Expand All @@ -22,8 +24,17 @@
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,
Expand Down
108 changes: 108 additions & 0 deletions cligj/features.py
Original file line number Diff line number Diff line change
@@ -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))
4 changes: 4 additions & 0 deletions tests/onepoint.geojson
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{"geometry": {"coordinates": [-122.7282, 45.5801], "type": "Point"},
"id": "0",
"properties": {},
"type": "Feature"}