geometamaker 0.1.2__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 CHANGED
@@ -1,7 +1,7 @@
1
1
  import importlib.metadata
2
2
 
3
3
  from .geometamaker import describe
4
- from .geometamaker import describe_dir
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', 'describe_dir', 'validate', 'validate_dir', 'Config', 'Profile')
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
- ROOT_LOGGER = logging.getLogger()
11
- ROOT_LOGGER.setLevel(logging.DEBUG)
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, or zip archives.')
24
- @click.argument('filepath', type=click.Path(exists=True))
25
- @click.option('-r', '--recursive', is_flag=True, default=False,
26
- help='if FILEPATH is a directory, describe files '
27
- 'in all subdirectories')
28
- @click.option('-nw', '--no-write', is_flag=True, default=False,
29
- help='Dump metadata to stdout instead of to a .yml file. '
30
- 'This option is ignored if `filepath` is a directory')
31
- def describe(filepath, recursive, no_write):
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
- if no_write:
34
- click.echo('the -nw, or --no-write, flag is ignored when '
35
- 'describing all files in a directory.')
36
- geometamaker.describe_dir(
37
- filepath, recursive=recursive)
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
- if no_write:
41
- click.echo(geometamaker.utils.yaml_dump(
42
- resource.model_dump(exclude=['metadata_path'])))
43
- else:
44
- resource.write()
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', type=click.Path(exists=True))
63
- @click.option('-r', '--recursive', is_flag=True, default=False,
64
- help='if `filepath` is a directory, validate documents '
65
- 'in all subdirectories.')
66
- def validate(filepath, recursive):
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, recursive=recursive)
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'Are you sure you want to delete {config.config_path}?',
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
- ROOT_LOGGER.addHandler(HANDLER)
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(__name__)
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 as err:
37
- LOGGER.debug('config file does not exist', exc_info=err)
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)