geometamaker 0.1.1__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geometamaker/__init__.py +2 -2
- geometamaker/cli.py +137 -31
- geometamaker/config.py +3 -4
- geometamaker/geometamaker.py +357 -130
- geometamaker/models.py +317 -114
- {geometamaker-0.1.1.dist-info → geometamaker-0.2.0.dist-info}/METADATA +44 -43
- geometamaker-0.2.0.dist-info/RECORD +12 -0
- {geometamaker-0.1.1.dist-info → geometamaker-0.2.0.dist-info}/WHEEL +1 -1
- geometamaker-0.1.1.dist-info/RECORD +0 -12
- {geometamaker-0.1.1.dist-info → geometamaker-0.2.0.dist-info}/entry_points.txt +0 -0
- {geometamaker-0.1.1.dist-info → geometamaker-0.2.0.dist-info/licenses}/LICENSE.txt +0 -0
- {geometamaker-0.1.1.dist-info → geometamaker-0.2.0.dist-info}/top_level.txt +0 -0
geometamaker/__init__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import importlib.metadata
|
|
2
2
|
|
|
3
3
|
from .geometamaker import describe
|
|
4
|
-
from .geometamaker import
|
|
4
|
+
from .geometamaker import describe_collection
|
|
5
5
|
from .geometamaker import validate
|
|
6
6
|
from .geometamaker import validate_dir
|
|
7
7
|
from .config import Config
|
|
@@ -10,4 +10,4 @@ from .models import Profile
|
|
|
10
10
|
|
|
11
11
|
__version__ = importlib.metadata.version('geometamaker')
|
|
12
12
|
|
|
13
|
-
__all__ = ('describe', '
|
|
13
|
+
__all__ = ('describe', 'describe_collection', 'validate', 'validate_dir', 'Config', 'Profile')
|
geometamaker/cli.py
CHANGED
|
@@ -3,45 +3,147 @@ import os
|
|
|
3
3
|
import sys
|
|
4
4
|
|
|
5
5
|
import click
|
|
6
|
+
import fsspec
|
|
7
|
+
import numpy
|
|
6
8
|
from pydantic import ValidationError
|
|
7
9
|
|
|
8
10
|
import geometamaker
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
LOGGER = logging.getLogger('geometamaker')
|
|
13
|
+
LOGGER.setLevel(logging.DEBUG)
|
|
12
14
|
HANDLER = logging.StreamHandler(sys.stdout)
|
|
13
15
|
FORMATTER = logging.Formatter(
|
|
14
16
|
fmt='%(asctime)s %(name)-18s %(levelname)-8s %(message)s',
|
|
15
17
|
datefmt='%m/%d/%Y %H:%M:%S ')
|
|
16
18
|
HANDLER.setFormatter(FORMATTER)
|
|
19
|
+
LOGGER.addFilter(
|
|
20
|
+
lambda record: not record.__dict__.get(
|
|
21
|
+
geometamaker.geometamaker._NOT_FOR_CLI, False))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# The recommended approach to allowing multiple ParamTypes
|
|
25
|
+
# https://github.com/pallets/click/issues/1729
|
|
26
|
+
class _ParamUnion(click.ParamType):
|
|
27
|
+
def __init__(self, types, report_all_errors=True):
|
|
28
|
+
"""Union of click.ParamTypes.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
types (list): List of click.ParamTypes to try to convert the value.
|
|
32
|
+
report_all_errors (bool): If True, all errors will be reported.
|
|
33
|
+
If False, only the last error will be reported.
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
self.types = types
|
|
37
|
+
self.report_all_errors = report_all_errors
|
|
38
|
+
|
|
39
|
+
def convert(self, value, param, ctx):
|
|
40
|
+
errors = []
|
|
41
|
+
for type_ in self.types:
|
|
42
|
+
try:
|
|
43
|
+
return type_.convert(value, param, ctx)
|
|
44
|
+
except click.BadParameter as e:
|
|
45
|
+
errors.append(e)
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
if self.report_all_errors:
|
|
49
|
+
self.fail(errors)
|
|
50
|
+
else:
|
|
51
|
+
# If errors from different types are expected to
|
|
52
|
+
# be very similar, just report the last one.
|
|
53
|
+
self.fail(errors.pop())
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# https://click.palletsprojects.com/en/stable/parameters/#how-to-implement-custom-types
|
|
57
|
+
class _URL(click.ParamType):
|
|
58
|
+
"""A type that asserts a URL exists."""
|
|
59
|
+
|
|
60
|
+
name = "url"
|
|
61
|
+
|
|
62
|
+
def convert(self, value, param, ctx):
|
|
63
|
+
of = fsspec.open(value)
|
|
64
|
+
if not of.fs.exists(value):
|
|
65
|
+
self.fail(f'{value} does not exist', param, ctx)
|
|
66
|
+
|
|
67
|
+
return value
|
|
17
68
|
|
|
18
69
|
|
|
19
70
|
@click.command(
|
|
20
71
|
help='''Describe properties of a dataset given by FILEPATH and write this
|
|
21
72
|
metadata to a .yml sidecar file. Or if FILEPATH is a directory, describe
|
|
22
73
|
all datasets within.''',
|
|
23
|
-
short_help='Generate metadata for geospatial or tabular data,
|
|
24
|
-
|
|
25
|
-
@click.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@click.option('-nw', '--no-write',
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
74
|
+
short_help='Generate metadata for geospatial or tabular data, compressed'
|
|
75
|
+
' archives, or collections of files in a directory.')
|
|
76
|
+
@click.argument('filepath',
|
|
77
|
+
type=_ParamUnion([click.Path(exists=True), _URL()],
|
|
78
|
+
report_all_errors=False))
|
|
79
|
+
@click.option('-nw', '--no-write',
|
|
80
|
+
is_flag=True,
|
|
81
|
+
default=False,
|
|
82
|
+
help='Dump metadata to stdout instead of to a .yml file.'
|
|
83
|
+
' This option is ignored when describing all files'
|
|
84
|
+
' in a directory.')
|
|
85
|
+
@click.option('-st', '--stats',
|
|
86
|
+
is_flag=True,
|
|
87
|
+
default=False,
|
|
88
|
+
help='Compute raster band statistics.')
|
|
89
|
+
@click.option('-d', '--depth',
|
|
90
|
+
default=numpy.iinfo(numpy.int16).max,
|
|
91
|
+
help='if FILEPATH is a directory, describe files in'
|
|
92
|
+
' subdirectories up to depth. Defaults to describing'
|
|
93
|
+
' all files.')
|
|
94
|
+
@click.option('-x', '--exclude',
|
|
95
|
+
default=None,
|
|
96
|
+
help='Regular expression used to exclude files from being'
|
|
97
|
+
' described. Only used if FILEPATH is a directory.')
|
|
98
|
+
@click.option('-a', '--all', 'all_files',
|
|
99
|
+
is_flag=True,
|
|
100
|
+
default=False,
|
|
101
|
+
help='Do not ignore files starting with .'
|
|
102
|
+
' Only used if FILEPATH is a directory.')
|
|
103
|
+
@click.option('-co', '--collection-only',
|
|
104
|
+
is_flag=True,
|
|
105
|
+
default=False,
|
|
106
|
+
help='If FILEPATH is a directory, do not write metadata documents'
|
|
107
|
+
' for all files in the directory. Only create a single'
|
|
108
|
+
' *-metadata.yml document for the collection')
|
|
109
|
+
def describe(filepath, depth, exclude, all_files, no_write, stats,
|
|
110
|
+
collection_only):
|
|
111
|
+
describing_single = True # if filepath is a file, or collection_only=True
|
|
32
112
|
if os.path.isdir(filepath):
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
113
|
+
resource = geometamaker.describe_collection(
|
|
114
|
+
filepath,
|
|
115
|
+
depth=depth,
|
|
116
|
+
exclude_regex=exclude,
|
|
117
|
+
exclude_hidden=(not all_files),
|
|
118
|
+
describe_files=(not collection_only),
|
|
119
|
+
compute_stats=stats)
|
|
120
|
+
describing_single = collection_only
|
|
38
121
|
else:
|
|
39
|
-
resource = geometamaker.describe(filepath)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
122
|
+
resource = geometamaker.describe(filepath, compute_stats=stats)
|
|
123
|
+
|
|
124
|
+
if no_write and describing_single:
|
|
125
|
+
click.echo(geometamaker.utils.yaml_dump(
|
|
126
|
+
resource._dump_for_write()))
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
if no_write and not describing_single:
|
|
130
|
+
click.echo('the -nw, or --no-write, flag is ignored when '
|
|
131
|
+
'describing all files in a directory.')
|
|
132
|
+
if resource._would_overwrite:
|
|
133
|
+
click.confirm(
|
|
134
|
+
f'\n{resource.metadata_path} is about to be overwritten'
|
|
135
|
+
' because it is not a valid metadata document.\n'
|
|
136
|
+
'Are you sure want to continue?',
|
|
137
|
+
abort=True)
|
|
138
|
+
try:
|
|
139
|
+
# Users can abort at the confirm and manage their own backups.
|
|
140
|
+
resource.write(backup=False)
|
|
141
|
+
except OSError:
|
|
142
|
+
click.echo(
|
|
143
|
+
f'geometamaker could not write to {resource.metadata_path}\n'
|
|
144
|
+
'Try using the --no-write flag to print metadata to '
|
|
145
|
+
'stdout instead:')
|
|
146
|
+
click.echo(f' geometamaker describe --no-write {filepath}')
|
|
45
147
|
|
|
46
148
|
|
|
47
149
|
def echo_validation_error(error, filepath):
|
|
@@ -59,14 +161,17 @@ def echo_validation_error(error, filepath):
|
|
|
59
161
|
help='''Validate a .yml metadata document given by FILEPATH.
|
|
60
162
|
Or if FILEPATH is a directory, validate all documents within.''',
|
|
61
163
|
short_help='Validate metadata documents for syntax or type errors.')
|
|
62
|
-
@click.argument('filepath',
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
164
|
+
@click.argument('filepath',
|
|
165
|
+
type=click.Path(exists=True))
|
|
166
|
+
@click.option('-d', '--depth',
|
|
167
|
+
default=numpy.iinfo(numpy.int16).max,
|
|
168
|
+
help='if FILEPATH is a directory, validate files in'
|
|
169
|
+
' subdirectories up to depth. Defaults to validating'
|
|
170
|
+
' all files.')
|
|
171
|
+
def validate(filepath, depth):
|
|
67
172
|
if os.path.isdir(filepath):
|
|
68
173
|
file_list, message_list = geometamaker.validate_dir(
|
|
69
|
-
filepath,
|
|
174
|
+
filepath, depth=depth)
|
|
70
175
|
for filepath, msg in zip(file_list, message_list):
|
|
71
176
|
if isinstance(msg, ValidationError):
|
|
72
177
|
echo_validation_error(msg, filepath)
|
|
@@ -96,7 +201,7 @@ def delete_config(ctx, param, value):
|
|
|
96
201
|
return
|
|
97
202
|
config = geometamaker.Config()
|
|
98
203
|
click.confirm(
|
|
99
|
-
f'
|
|
204
|
+
f'\nAre you sure you want to delete {config.config_path}?',
|
|
100
205
|
abort=True)
|
|
101
206
|
config.delete()
|
|
102
207
|
ctx.exit()
|
|
@@ -141,7 +246,8 @@ def config(individual_name, email, organization, position_name,
|
|
|
141
246
|
click.echo(f'saved profile information to {config.config_path}')
|
|
142
247
|
|
|
143
248
|
|
|
144
|
-
@click.group(
|
|
249
|
+
@click.group(
|
|
250
|
+
epilog='https://geometamaker.readthedocs.io/en/latest/ for more details')
|
|
145
251
|
@click.option('-v', 'verbosity', count=True, default=2, required=False,
|
|
146
252
|
help='''Override the default verbosity of logging. Use "-vvv" for
|
|
147
253
|
debug-level logging. Omit this flag for default,
|
|
@@ -150,7 +256,7 @@ def config(individual_name, email, organization, position_name,
|
|
|
150
256
|
def cli(verbosity):
|
|
151
257
|
log_level = logging.ERROR - verbosity*10
|
|
152
258
|
HANDLER.setLevel(log_level)
|
|
153
|
-
|
|
259
|
+
LOGGER.addHandler(HANDLER)
|
|
154
260
|
|
|
155
261
|
|
|
156
262
|
cli.add_command(describe)
|
geometamaker/config.py
CHANGED
|
@@ -7,7 +7,7 @@ from pydantic import ValidationError
|
|
|
7
7
|
from . import models
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
LOGGER = logging.getLogger(
|
|
10
|
+
LOGGER = logging.getLogger('geometamaker')
|
|
11
11
|
|
|
12
12
|
CONFIG_FILENAME = 'geometamaker_profile.yml'
|
|
13
13
|
|
|
@@ -33,9 +33,8 @@ class Config(object):
|
|
|
33
33
|
|
|
34
34
|
try:
|
|
35
35
|
self.profile = models.Profile.load(self.config_path)
|
|
36
|
-
except FileNotFoundError
|
|
37
|
-
LOGGER.debug('config file does not exist'
|
|
38
|
-
pass
|
|
36
|
+
except FileNotFoundError:
|
|
37
|
+
LOGGER.debug(f'config file does not exist at {self.config_path}')
|
|
39
38
|
# an invalid profile should raise a ValidationError
|
|
40
39
|
except ValidationError as err:
|
|
41
40
|
LOGGER.warning('', exc_info=err)
|