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.
@@ -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)