dump-things-pyclient 0.2.2__py3-none-any.whl → 0.2.3__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,150 @@
1
+ import json
2
+ import logging
3
+ import sys
4
+ from functools import partial
5
+
6
+ import rich_click as click
7
+
8
+ from ...communicate import (
9
+ HTTPError,
10
+ collection_delete_record,
11
+ curated_delete_record,
12
+ incoming_delete_record,
13
+ incoming_read_labels,
14
+ )
15
+
16
+
17
+ subcommand_name = 'delete-records'
18
+
19
+ logger = logging.getLogger('delete-records')
20
+
21
+
22
+ @click.command(short_help='Delete records from a dump-things collection')
23
+ @click.pass_obj
24
+ @click.argument(
25
+ 'service_url',
26
+ metavar='SERVICE_URL',
27
+ )
28
+ @click.argument(
29
+ 'collection',
30
+ metavar='COLLECTION',
31
+ )
32
+ @click.argument(
33
+ 'pids',
34
+ metavar='PIDS',
35
+ nargs=-1,
36
+ )
37
+ @click.option(
38
+ '--curated', '-c',
39
+ default=False,
40
+ is_flag=True,
41
+ help='delete record from the curated area of the collection. (Note: requires a token with curator rights)',
42
+ )
43
+ @click.option(
44
+ '--incoming', '-i',
45
+ metavar='LABEL',
46
+ help='delete from the collection\'s inbox with label LABEL, if LABEL is "-", return labels of all collection inboxes and exit',
47
+ )
48
+ @click.option(
49
+ '--ignore-errors',
50
+ default=False,
51
+ is_flag=True,
52
+ help='ignore errors when deleting a pid and continue with remaining pids',
53
+ )
54
+ def cli(
55
+ obj,
56
+ service_url,
57
+ collection,
58
+ pids,
59
+ curated,
60
+ incoming,
61
+ ignore_errors,
62
+ ):
63
+ """Delete records from a collection on a dump-things-service
64
+
65
+ This command delete the records given by PIDS from the collection COLLECTION
66
+ of the dump-things service SERVICE_URL. If no PIDS are provided on the
67
+ command line, the pids that should be deleted are read from stdin (one pid
68
+ per line, lines are stripped).
69
+
70
+ By default, the records will be deleted from the inbox associated with the
71
+ token. If the option -c/--curated is given, the records are deleted from
72
+ the curated area of the collection (this requires a token with
73
+ curator rights).
74
+ """
75
+ try:
76
+ return delete_records(
77
+ obj,
78
+ service_url,
79
+ collection,
80
+ pids,
81
+ curated,
82
+ incoming,
83
+ ignore_errors,
84
+ )
85
+ except HTTPError as e:
86
+ click.echo(f'ERROR: {e}: {e.response.text}', err=True)
87
+ return 1
88
+
89
+
90
+ def delete_records(
91
+ obj,
92
+ service_url,
93
+ collection,
94
+ pids,
95
+ curated,
96
+ incoming,
97
+ ignore_errors,
98
+ ):
99
+ token = obj
100
+
101
+ if token is None:
102
+ click.echo(f'WARNING: no token provided', err=True)
103
+
104
+ if incoming and curated:
105
+ click.echo(
106
+ 'ERROR: -i/--incoming and -c/--curated are mutually exclusive',
107
+ err=True,
108
+ )
109
+ return 1
110
+
111
+ kwargs = dict(
112
+ service_url=service_url,
113
+ collection=collection,
114
+ token=token,
115
+ )
116
+
117
+ if incoming == '-':
118
+ result = incoming_read_labels(**kwargs)
119
+ click.echo('\n'.join(
120
+ map(
121
+ partial(json.dumps, ensure_ascii=False),
122
+ result
123
+ )
124
+ ))
125
+ return 0
126
+
127
+ if incoming:
128
+ operation = partial(incoming_delete_record, label=incoming)
129
+ elif curated:
130
+ operation = curated_delete_record
131
+ else:
132
+ operation = collection_delete_record
133
+
134
+ if not pids:
135
+ pids = sys.stdin
136
+
137
+ for pid in pids:
138
+ try:
139
+ operation(
140
+ service_url=service_url,
141
+ collection=collection,
142
+ pid=pid.strip(),
143
+ token=token,
144
+ )
145
+ except HTTPError as e:
146
+ if ignore_errors:
147
+ click.echo(f'ERROR: while deleting pid {pid}: {e}', err=True)
148
+ continue
149
+ raise
150
+ return 0
@@ -0,0 +1,194 @@
1
+ import json
2
+ from collections import defaultdict
3
+ from itertools import count
4
+ from pathlib import Path
5
+ from typing import (
6
+ Any,
7
+ Iterable,
8
+ )
9
+
10
+ import rich_click as click
11
+
12
+ from ...communicate import (
13
+ HTTPError,
14
+ curated_read_records,
15
+ incoming_read_labels,
16
+ incoming_read_records,
17
+ server,
18
+ )
19
+
20
+
21
+ subcommand_name = 'export'
22
+
23
+
24
+ @click.command(short_help='Export a collection to the file system')
25
+ @click.pass_obj
26
+ @click.argument(
27
+ 'service_url',
28
+ metavar='SERVICE_URL',
29
+ )
30
+ @click.argument(
31
+ 'collection',
32
+ metavar='COLLECTION',
33
+ )
34
+ @click.argument(
35
+ 'destination',
36
+ type=click.Path(
37
+ exists=False,
38
+ file_okay=False,
39
+ dir_okay=True,
40
+ writable=True,
41
+ allow_dash=False,
42
+ path_type=Path,
43
+ ),
44
+ metavar='DESTINATION_DIR',
45
+ )
46
+ @click.option(
47
+ '--ignore-errors',
48
+ default=False,
49
+ is_flag=True,
50
+ help='ignore records with missing `schema_type` instead of raising an error',
51
+ )
52
+ def cli(
53
+ obj: Any,
54
+ service_url: str,
55
+ collection: str,
56
+ destination: Path,
57
+ ignore_errors,
58
+ ):
59
+ """Export a collection to disk
60
+
61
+ This command exports all records that are stored in curated area and in the
62
+ incoming areas of collection COLLECTION of the dump-things service
63
+ SERVICE_URL.
64
+
65
+ Exported records are written to the directory DESTINATION_DIR.
66
+ DESTINATION_DIR must not exist, `export` will create it.
67
+
68
+ A token with curator rights has to be provided.
69
+ """
70
+ try:
71
+ return export(
72
+ obj,
73
+ service_url,
74
+ collection,
75
+ destination,
76
+ ignore_errors,
77
+ )
78
+ except HTTPError as e:
79
+ click.echo(f'ERROR: {e}: {e.response.text}', err=True)
80
+ except ValueError as e:
81
+ click.echo(f'ERROR: {e}', err=True)
82
+ return 1
83
+
84
+
85
+ def export(
86
+ obj: Any,
87
+ service_url: str,
88
+ collection: str,
89
+ destination: Path,
90
+ ignore_errors: bool,
91
+ ):
92
+ token = obj
93
+
94
+ if token is None:
95
+ click.echo(f'ERROR: no token provided', err=True)
96
+ return 1
97
+
98
+ server_info = server(service_url)
99
+ collection_info = ([c for c in server_info['collections'] if c['name'] == collection] or None)[0]
100
+
101
+ if not collection_info:
102
+ click.echo(f'ERROR: no collection {collection} on service', err=True)
103
+ return 1
104
+
105
+ description = {
106
+ 'name': collection,
107
+ 'schema': collection_info['schema'],
108
+ }
109
+
110
+ destination.mkdir(parents=True, exist_ok=False)
111
+ (destination / 'description.json').write_text(
112
+ json.dumps(description, ensure_ascii=False),
113
+ )
114
+
115
+ # Store the curated records
116
+ curated_destination = destination / 'curated'
117
+ curated_destination.mkdir()
118
+
119
+ _store_records(
120
+ map(
121
+ lambda x: x[0],
122
+ curated_read_records(
123
+ service_url=service_url,
124
+ collection=collection,
125
+ token=token,
126
+ )
127
+ ),
128
+ curated_destination,
129
+ ignore_errors,
130
+ )
131
+
132
+ # Store the incoming records
133
+ for label in incoming_read_labels(
134
+ service_url=service_url,
135
+ collection=collection,
136
+ token=token,
137
+ ):
138
+ incoming_destination = destination / 'incoming' / label
139
+ incoming_destination.mkdir(parents=True, exist_ok=False)
140
+ _store_records(
141
+ map(
142
+ lambda x: x[0],
143
+ incoming_read_records(
144
+ service_url=service_url,
145
+ collection=collection,
146
+ label=label,
147
+ token=token,
148
+ )
149
+ ),
150
+ incoming_destination,
151
+ ignore_errors,
152
+ )
153
+
154
+ return 0
155
+
156
+
157
+ def _store_records(
158
+ source: Iterable,
159
+ destination: Path,
160
+ ignore_errors: bool = False,
161
+ ):
162
+ created_dirs = set()
163
+ class_counters = defaultdict(count)
164
+
165
+ for record in source:
166
+ class_name = _de_prefix(record.get('schema_type', None))
167
+ if class_name is None:
168
+ if ignore_errors:
169
+ click.echo(
170
+ f'WARNING: no `schema_type` in record `{record["pid"]}`',
171
+ err=True
172
+ )
173
+ continue
174
+ msg = f'no `schema_type` in record `{record["pid"]}`'
175
+ raise ValueError(msg)
176
+
177
+ next_name_for_class = f'{next(class_counters[class_name]):09d}.json'
178
+ file_dir, file_name = (
179
+ destination / class_name / next_name_for_class[:3],
180
+ next_name_for_class[3:]
181
+ )
182
+ if file_dir not in created_dirs:
183
+ file_dir.mkdir(parents=True, exist_ok=False)
184
+ created_dirs.add(file_dir)
185
+
186
+ (file_dir / file_name).write_text(
187
+ json.dumps(record, indent=2, ensure_ascii=False),
188
+ )
189
+
190
+
191
+ def _de_prefix(
192
+ name: str,
193
+ ):
194
+ return name.split(':', 1)[-1]
@@ -33,8 +33,6 @@ subcommand_name = 'get-records'
33
33
  )
34
34
  @click.option(
35
35
  '--class', '-C', 'cls',
36
- default=False,
37
- is_flag=True,
38
36
  help='only read records of this class, ignored if "--pid" is provided',
39
37
  )
40
38
  @click.option(
@@ -50,7 +48,7 @@ subcommand_name = 'get-records'
50
48
  @click.option(
51
49
  '--incoming', '-i',
52
50
  metavar='LABEL',
53
- help='read from the collection inbox with label LABEL, if LABEL is "-", return labels of all collection inboxes',
51
+ help='read from the collection\'s inbox with label LABEL, if LABEL is "-", return labels of all collection inboxes and exit',
54
52
  )
55
53
  @click.option(
56
54
  '--curated', '-c',
@@ -60,8 +58,6 @@ subcommand_name = 'get-records'
60
58
  )
61
59
  @click.option(
62
60
  '--matching', '-m',
63
- default=False,
64
- is_flag=True,
65
61
  help='return only records that have a matching value (use % as wildcard). Ignored if "--pid" is provided. (Note: not all endpoints and backends support matching)',
66
62
  )
67
63
  @click.option(
@@ -188,7 +184,7 @@ def get_records(
188
184
 
189
185
  if incoming == '-':
190
186
  result = incoming_read_labels(**kwargs)
191
- print('\n'.join(
187
+ click.echo('\n'.join(
192
188
  map(
193
189
  partial(json.dumps, ensure_ascii=False),
194
190
  result)))
@@ -677,6 +677,23 @@ def incoming_delete_record(
677
677
  params={'pid': pid})
678
678
 
679
679
 
680
+ def server(
681
+ service_url: str,
682
+ ) -> JSON:
683
+ """Get server-information from the service
684
+
685
+ :param service_url: the base URL of the service, i.e., the URL up to
686
+ `/<collection>/...` or `/server`
687
+
688
+ :return: information returned by the `<service_url>/server` endpoint
689
+ """
690
+ url = (
691
+ (f'{service_url[:-1]}' if service_url.endswith('/') else service_url)
692
+ + '/server'
693
+ )
694
+ return _do_request(requests.get, url=url, token=None, params=None)
695
+
696
+
680
697
  def _get_from_url(url: str,
681
698
  token: str | None,
682
699
  params: dict[str, str] | None = None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dump-things-pyclient
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: A client library and CLI commands for dump-things-services
5
5
  Author-email: Christian Mönch <christian.moench@web.de>
6
6
  Requires-Python: >=3.11
@@ -1,5 +1,5 @@
1
1
  dump_things_pyclient/__init__.py,sha256=cn-U3TRIalN6aYHp1cMBRkQm1x98XBwquLFbgFEIf_Q,113
2
- dump_things_pyclient/communicate.py,sha256=CTkgEigu16BmLrlJScMQKhHLI_W4SywDb4HCwAICWzA,30523
2
+ dump_things_pyclient/communicate.py,sha256=R1lifyuP5Cm2oQdyaWxTsQAbiMBU9NxYA5-9yU4M3qA,31008
3
3
  dump_things_pyclient/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  dump_things_pyclient/commands/dtc.py,sha256=dxW5RuogqwhzfVujZ_EEsQMk8BcVMbZyMdg5c8EvYIA,1726
5
5
  dump_things_pyclient/commands/json2ttl.py,sha256=8BkvdjLWZ_H0L6fTmuR2M2MglKiMUiuNUcuWr_w6_dQ,2133
@@ -7,12 +7,14 @@ dump_things_pyclient/commands/redirect.py,sha256=kl8pGj8khjxk4lhk8AJLfgtCIm5Ptje
7
7
  dump_things_pyclient/commands/dtc_plugins/__init__.py,sha256=0YLByLiofhHkhJcDCkokldcCw3Jj0rsKJinRX4tt3Hc,514
8
8
  dump_things_pyclient/commands/dtc_plugins/auto_curate.py,sha256=3_SHXPQCXmY6GqTMTNVkKh5vvshfiZpMGFY0gvJxRbo,7411
9
9
  dump_things_pyclient/commands/dtc_plugins/clean_incoming.py,sha256=slk3xn1-DgMl88WZqgyemyscwof97TMXt3rley4mU1w,2086
10
- dump_things_pyclient/commands/dtc_plugins/get_records.py,sha256=YBRNo7HUCWZ-EOv8EU_yGMRRoXqqKy1XTcsfQ64ymuk,7508
10
+ dump_things_pyclient/commands/dtc_plugins/delete_record.py,sha256=TVXyGetob8d75zuSBGuRKjtQPAcRqqjw-CcwYYHSC28,3626
11
+ dump_things_pyclient/commands/dtc_plugins/export.py,sha256=FF1DmSmIA3GIhosUrTrXy_qyypMSJUalxdLJ844XhIE,4850
12
+ dump_things_pyclient/commands/dtc_plugins/get_records.py,sha256=00W-agALqO_qFhjq14MI9N6TnJCgscMioy-Ll1ClrB4,7451
11
13
  dump_things_pyclient/commands/dtc_plugins/list_incoming.py,sha256=tmM0Qs4MVwMMLyERsWCxWGTM90rSNOShLpHH32wObd8,1959
12
14
  dump_things_pyclient/commands/dtc_plugins/post_records.py,sha256=s3j9THe-RszKxyIISkQZRCTKplWWLlomHbS5dyRlep0,2908
13
15
  dump_things_pyclient/commands/dtc_plugins/read_pages.py,sha256=hpw7vtG7joIMrNqEqZFCwzbQFd3ATzv7iyySsX8nKWk,3385
14
- dump_things_pyclient-0.2.2.dist-info/METADATA,sha256=kvCtVMiZmCjtrJssE6O-WGGZBVrpVec2uoXdOcaj8vM,999
15
- dump_things_pyclient-0.2.2.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
16
- dump_things_pyclient-0.2.2.dist-info/entry_points.txt,sha256=U1QhQtk767G_OXdZwPdTXYbIPfcDU13Z2u1d6exX8uE,470
17
- dump_things_pyclient-0.2.2.dist-info/top_level.txt,sha256=Asvruw-SyLoYhWis1CFOx89RGxpjXoTZVGoq4JSGt88,21
18
- dump_things_pyclient-0.2.2.dist-info/RECORD,,
16
+ dump_things_pyclient-0.2.3.dist-info/METADATA,sha256=hgtqkFRDuld7OTe2Alt-U-2YDqZurOqYT7PHphJ-uGQ,999
17
+ dump_things_pyclient-0.2.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
18
+ dump_things_pyclient-0.2.3.dist-info/entry_points.txt,sha256=U1QhQtk767G_OXdZwPdTXYbIPfcDU13Z2u1d6exX8uE,470
19
+ dump_things_pyclient-0.2.3.dist-info/top_level.txt,sha256=Asvruw-SyLoYhWis1CFOx89RGxpjXoTZVGoq4JSGt88,21
20
+ dump_things_pyclient-0.2.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5