geometamaker 0.1.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 +13 -0
- geometamaker/cli.py +158 -0
- geometamaker/config.py +65 -0
- geometamaker/geometamaker.py +529 -0
- geometamaker/models.py +653 -0
- geometamaker/utils.py +30 -0
- geometamaker-0.1.0.dist-info/LICENSE.txt +202 -0
- geometamaker-0.1.0.dist-info/METADATA +400 -0
- geometamaker-0.1.0.dist-info/RECORD +12 -0
- geometamaker-0.1.0.dist-info/WHEEL +5 -0
- geometamaker-0.1.0.dist-info/entry_points.txt +2 -0
- geometamaker-0.1.0.dist-info/top_level.txt +1 -0
geometamaker/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
|
|
3
|
+
from .geometamaker import describe
|
|
4
|
+
from .geometamaker import describe_dir
|
|
5
|
+
from .geometamaker import validate
|
|
6
|
+
from .geometamaker import validate_dir
|
|
7
|
+
from .config import Config
|
|
8
|
+
from .models import Profile
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__version__ = importlib.metadata.version('geometamaker')
|
|
12
|
+
|
|
13
|
+
__all__ = ('describe', 'describe_dir', 'validate', 'validate_dir', 'Config', 'Profile')
|
geometamaker/cli.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from pydantic import ValidationError
|
|
7
|
+
|
|
8
|
+
import geometamaker
|
|
9
|
+
|
|
10
|
+
ROOT_LOGGER = logging.getLogger()
|
|
11
|
+
ROOT_LOGGER.setLevel(logging.DEBUG)
|
|
12
|
+
HANDLER = logging.StreamHandler(sys.stdout)
|
|
13
|
+
FORMATTER = logging.Formatter(
|
|
14
|
+
fmt='%(asctime)s %(name)-18s %(levelname)-8s %(message)s',
|
|
15
|
+
datefmt='%m/%d/%Y %H:%M:%S ')
|
|
16
|
+
HANDLER.setFormatter(FORMATTER)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@click.command(
|
|
20
|
+
help='''Describe properties of a dataset given by FILEPATH and write this
|
|
21
|
+
metadata to a .yml sidecar file. Or if FILEPATH is a directory, describe
|
|
22
|
+
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):
|
|
32
|
+
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)
|
|
38
|
+
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()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def echo_validation_error(error, filepath):
|
|
48
|
+
summary = u'\u2715' + f' {filepath}: {error.error_count()} validation errors'
|
|
49
|
+
click.secho(summary, fg='bright_red')
|
|
50
|
+
for e in error.errors():
|
|
51
|
+
location = ', '.join(e['loc'])
|
|
52
|
+
msg_string = (f" {e['msg']}. [input_value={e['input']}, "
|
|
53
|
+
f"input_type={type(e['input']).__name__}]")
|
|
54
|
+
click.secho(location, bold=True)
|
|
55
|
+
click.secho(msg_string)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@click.command(
|
|
59
|
+
help='''Validate a .yml metadata document given by FILEPATH.
|
|
60
|
+
Or if FILEPATH is a directory, validate all documents within.''',
|
|
61
|
+
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):
|
|
67
|
+
if os.path.isdir(filepath):
|
|
68
|
+
file_list, message_list = geometamaker.validate_dir(
|
|
69
|
+
filepath, recursive=recursive)
|
|
70
|
+
for filepath, msg in zip(file_list, message_list):
|
|
71
|
+
if isinstance(msg, ValidationError):
|
|
72
|
+
echo_validation_error(msg, filepath)
|
|
73
|
+
else:
|
|
74
|
+
color = 'yellow'
|
|
75
|
+
icon = u'\u25CB'
|
|
76
|
+
if not msg:
|
|
77
|
+
color = 'bright_green'
|
|
78
|
+
icon = u'\u2713'
|
|
79
|
+
click.secho(f'{icon} {filepath} {msg}', fg=color)
|
|
80
|
+
else:
|
|
81
|
+
error = geometamaker.validate(filepath)
|
|
82
|
+
if error:
|
|
83
|
+
echo_validation_error(error, filepath)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def print_config(ctx, param, value):
|
|
87
|
+
if not value or ctx.resilient_parsing:
|
|
88
|
+
return
|
|
89
|
+
config = geometamaker.Config()
|
|
90
|
+
click.echo(config)
|
|
91
|
+
ctx.exit()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def delete_config(ctx, param, value):
|
|
95
|
+
if not value or ctx.resilient_parsing:
|
|
96
|
+
return
|
|
97
|
+
config = geometamaker.Config()
|
|
98
|
+
click.confirm(
|
|
99
|
+
f'Are you sure you want to delete {config.config_path}?',
|
|
100
|
+
abort=True)
|
|
101
|
+
config.delete()
|
|
102
|
+
ctx.exit()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@click.command(
|
|
106
|
+
short_help='''Configure GeoMetaMaker with information to apply to all
|
|
107
|
+
metadata descriptions''',
|
|
108
|
+
help='''When prompted, enter contact and data-license information
|
|
109
|
+
that will be stored in a user profile. This information will automatically
|
|
110
|
+
populate contact and license sections of any metadata described on your
|
|
111
|
+
system. Press enter to leave any field blank.''')
|
|
112
|
+
@click.option('--individual-name', prompt=True, default='')
|
|
113
|
+
@click.option('--email', prompt=True, default='')
|
|
114
|
+
@click.option('--organization', prompt=True, default='')
|
|
115
|
+
@click.option('--position-name', prompt=True, default='')
|
|
116
|
+
@click.option('--license-title', prompt=True, default='',
|
|
117
|
+
help='the name of a data license, e.g. "CC-BY-4.0"')
|
|
118
|
+
@click.option('--license-url', prompt=True, default='',
|
|
119
|
+
help='a url for a data license')
|
|
120
|
+
@click.option('-p', '--print', is_flag=True, is_eager=True,
|
|
121
|
+
callback=print_config, expose_value=False,
|
|
122
|
+
help='Print your current GeoMetaMaker configuration.')
|
|
123
|
+
@click.option('--delete', is_flag=True, is_eager=True,
|
|
124
|
+
callback=delete_config, expose_value=False,
|
|
125
|
+
help='Delete your configuration file.')
|
|
126
|
+
def config(individual_name, email, organization, position_name,
|
|
127
|
+
license_url, license_title):
|
|
128
|
+
contact = geometamaker.models.ContactSchema()
|
|
129
|
+
contact.individual_name = individual_name
|
|
130
|
+
contact.email = email
|
|
131
|
+
contact.organization = organization
|
|
132
|
+
contact.position_name = position_name
|
|
133
|
+
|
|
134
|
+
license = geometamaker.models.LicenseSchema()
|
|
135
|
+
license.path = license_url
|
|
136
|
+
license.title = license_title
|
|
137
|
+
|
|
138
|
+
profile = geometamaker.models.Profile(contact=contact, license=license)
|
|
139
|
+
config = geometamaker.Config()
|
|
140
|
+
config.save(profile)
|
|
141
|
+
click.echo(f'saved profile information to {config.config_path}')
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@click.group()
|
|
145
|
+
@click.option('-v', 'verbosity', count=True, default=2, required=False,
|
|
146
|
+
help='''Override the default verbosity of logging. Use "-vvv" for
|
|
147
|
+
debug-level logging. Omit this flag for default,
|
|
148
|
+
info-level logging.''')
|
|
149
|
+
@click.version_option(message="%(version)s")
|
|
150
|
+
def cli(verbosity):
|
|
151
|
+
log_level = logging.ERROR - verbosity*10
|
|
152
|
+
HANDLER.setLevel(log_level)
|
|
153
|
+
ROOT_LOGGER.addHandler(HANDLER)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
cli.add_command(describe)
|
|
157
|
+
cli.add_command(validate)
|
|
158
|
+
cli.add_command(config)
|
geometamaker/config.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import platformdirs
|
|
5
|
+
from pydantic import ValidationError
|
|
6
|
+
|
|
7
|
+
from . import models
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
LOGGER = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
CONFIG_FILENAME = 'geometamaker_profile.yml'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Config(object):
|
|
16
|
+
"""Encapsulates user-settings such as a metadata Profile."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, config_path=None):
|
|
19
|
+
"""Load a Profile from a config file.
|
|
20
|
+
|
|
21
|
+
Use a default user profile if none given.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
config_path (str): path to a local yaml file
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
if config_path is None:
|
|
28
|
+
self.config_path = os.path.join(
|
|
29
|
+
platformdirs.user_config_dir(), CONFIG_FILENAME)
|
|
30
|
+
else:
|
|
31
|
+
self.config_path = config_path
|
|
32
|
+
self.profile = models.Profile()
|
|
33
|
+
|
|
34
|
+
try:
|
|
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
|
|
39
|
+
# an invalid profile should raise a ValidationError
|
|
40
|
+
except ValidationError as err:
|
|
41
|
+
LOGGER.warning('', exc_info=err)
|
|
42
|
+
LOGGER.warning(
|
|
43
|
+
f'{self.config_path} contains an inavlid profile. '
|
|
44
|
+
'It will be ignored. You may wish to delete() it.')
|
|
45
|
+
|
|
46
|
+
def __repr__(self):
|
|
47
|
+
"""Represent config as a string."""
|
|
48
|
+
return f'Config(config_path={self.config_path} profile={self.profile})'
|
|
49
|
+
|
|
50
|
+
def save(self, profile):
|
|
51
|
+
"""Save a Profile to a local config file.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
profile (geometamaker.models.Profile)
|
|
55
|
+
"""
|
|
56
|
+
LOGGER.info(f'writing profile to {self.config_path}')
|
|
57
|
+
profile.write(self.config_path)
|
|
58
|
+
|
|
59
|
+
def delete(self):
|
|
60
|
+
"""Delete the config file."""
|
|
61
|
+
try:
|
|
62
|
+
os.remove(self.config_path)
|
|
63
|
+
LOGGER.info(f'removed {self.config_path}')
|
|
64
|
+
except FileNotFoundError as error:
|
|
65
|
+
LOGGER.debug(error)
|