girder-jsonforms 2.1.2__tar.gz → 2.1.4__tar.gz

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.
Files changed (72) hide show
  1. {girder_jsonforms-2.1.2/girder_jsonforms.egg-info → girder_jsonforms-2.1.4}/PKG-INFO +2 -10
  2. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/lib/igsn_client.py +45 -4
  3. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/lib/igsn_vocab.py +43 -1
  4. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/rest/deposition.py +5 -1
  5. girder_jsonforms-2.1.4/girder_jsonforms/scripts/reconcile_igsn.py +118 -0
  6. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/web_client/dist/girder-plugin-jsonforms.umd.cjs +10 -8
  7. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/web_client/dist/style.css +1 -1
  8. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/worker_plugin/igsn_registry.py +123 -4
  9. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4/girder_jsonforms.egg-info}/PKG-INFO +2 -10
  10. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms.egg-info/SOURCES.txt +2 -4
  11. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms.egg-info/entry_points.txt +1 -0
  12. girder_jsonforms-2.1.4/pyproject.toml +75 -0
  13. girder_jsonforms-2.1.2/.github/workflows/release.yml +0 -102
  14. girder_jsonforms-2.1.2/.gitignore +0 -136
  15. girder_jsonforms-2.1.2/girder_jsonforms.egg-info/not-zip-safe +0 -1
  16. girder_jsonforms-2.1.2/setup.py +0 -67
  17. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/LICENSE +0 -0
  18. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/README.md +0 -0
  19. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/doc/file-uploads.md +0 -0
  20. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/doc/preload.md +0 -0
  21. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/__init__.py +0 -0
  22. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/asgi.py +0 -0
  23. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/lib/__init__.py +0 -0
  24. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/lib/announcement.py +0 -0
  25. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/lib/events.py +0 -0
  26. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/lib/google_drive.py +0 -0
  27. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/lib/jq.py +0 -0
  28. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/lib/locks.py +0 -0
  29. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/lib/mail.py +0 -0
  30. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/lib/metadata_dates.py +0 -0
  31. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/lib/orcid.py +0 -0
  32. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/lib/project_helpers.py +0 -0
  33. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/lib/project_tasks.py +0 -0
  34. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/lib/response_cache.py +0 -0
  35. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/lib/timing.py +0 -0
  36. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/mail_assets/caimee-shield-80.png +0 -0
  37. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/mail_templates/_projectsFooter.mako +0 -0
  38. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/mail_templates/_projectsFooter.txt.mako +0 -0
  39. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/mail_templates/_projectsHeader.mako +0 -0
  40. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/mail_templates/projectDecision.mako +0 -0
  41. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/mail_templates/projectDecision.txt.mako +0 -0
  42. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/mail_templates/projectSubmitted.mako +0 -0
  43. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/mail_templates/projectSubmitted.txt.mako +0 -0
  44. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/models/__init__.py +0 -0
  45. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/models/deposition.py +0 -0
  46. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/models/entry.py +0 -0
  47. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/models/form.py +0 -0
  48. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/models/project.py +0 -0
  49. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/rest/__init__.py +0 -0
  50. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/rest/aimdl.py +0 -0
  51. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/rest/entry.py +0 -0
  52. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/rest/form.py +0 -0
  53. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/rest/project.py +0 -0
  54. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/schemas/__init__.py +0 -0
  55. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/schemas/datacite-v4.5.json +0 -0
  56. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/scripts/__init__.py +0 -0
  57. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/scripts/migrate_metadata_dates.py +0 -0
  58. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/scripts/preview_mail.py +0 -0
  59. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/settings.py +0 -0
  60. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/worker_plugin/__init__.py +0 -0
  61. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/worker_plugin/amdee.py +0 -0
  62. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/worker_plugin/folder_ops.py +0 -0
  63. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/worker_plugin/handle_enumsource.py +0 -0
  64. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/worker_plugin/htmax.py +0 -0
  65. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/worker_plugin/orcid.py +0 -0
  66. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/worker_plugin/projects.py +0 -0
  67. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/worker_plugin/pull_related_ids.py +0 -0
  68. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms/worker_plugin/relate_entry_to_igsn.py +0 -0
  69. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms.egg-info/dependency_links.txt +0 -0
  70. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms.egg-info/requires.txt +0 -0
  71. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/girder_jsonforms.egg-info/top_level.txt +0 -0
  72. {girder_jsonforms-2.1.2 → girder_jsonforms-2.1.4}/setup.cfg +0 -0
@@ -1,11 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: girder-jsonforms
3
- Version: 2.1.2
3
+ Version: 2.1.4
4
4
  Summary: Girder plugin adding forms based on JSON-editor
5
- License: BSD-3-Clause
5
+ License-Expression: BSD-3-Clause
6
6
  Classifier: Development Status :: 4 - Beta
7
7
  Classifier: Environment :: Web Environment
8
- Classifier: License :: OSI Approved :: BSD License
9
8
  Classifier: Operating System :: POSIX :: Linux
10
9
  Classifier: Programming Language :: Python
11
10
  Classifier: Programming Language :: Python :: 3
@@ -23,14 +22,7 @@ Requires-Dist: google-auth-oauthlib
23
22
  Requires-Dist: jsondiff
24
23
  Requires-Dist: pandas
25
24
  Requires-Dist: openpyxl
26
- Dynamic: classifier
27
- Dynamic: description
28
- Dynamic: description-content-type
29
- Dynamic: license
30
25
  Dynamic: license-file
31
- Dynamic: requires-dist
32
- Dynamic: requires-python
33
- Dynamic: summary
34
26
 
35
27
  ### Girder JSONForms Plugin
36
28
 
@@ -19,6 +19,7 @@ Two rules matter here:
19
19
  import logging
20
20
 
21
21
  import requests
22
+ from girder.exceptions import RestException
22
23
  from girder.models.setting import Setting
23
24
  from requests.adapters import HTTPAdapter
24
25
  from urllib3.util.retry import Retry
@@ -29,16 +30,40 @@ logger = logging.getLogger(__name__)
29
30
 
30
31
  DEFAULT_TIMEOUT = 30
31
32
 
33
+ # Upstream statuses that mean "the caller asked for something the registry will
34
+ # not do": a prefix outside the tenant's vocabulary, an identifier that already
35
+ # exists, a published record that cannot be withdrawn. The service's own
36
+ # explanation is the useful part and the person filling in the form can act on
37
+ # it, so these become a 400 carrying that message.
38
+ #
39
+ # Everything else -- our service token rejected, a 5xx, an unreachable host --
40
+ # is this deployment's problem rather than the caller's, so it becomes a 502
41
+ # and the detail is left to the log.
42
+ CLIENT_ERROR_STATUSES = frozenset({400, 404, 409, 422})
32
43
 
33
- class IGSNServiceError(Exception):
44
+
45
+ def _rest_code(status_code):
46
+ return 400 if status_code in CLIENT_ERROR_STATUSES else 502
47
+
48
+
49
+ class IGSNServiceError(RestException):
34
50
  """The IGSN registry service could not satisfy a request.
35
51
 
36
52
  Deliberately fatal to whatever operation raised it: a Girder deposition must
37
53
  never exist without a matching record in the registry.
54
+
55
+ It is a ``RestException`` so that the fatality is a *useful* one. Identifier
56
+ allocation happens several levels below the endpoint -- in
57
+ ``PrefixCounter.get_next``, and off the ``model.entry.save`` event for the
58
+ form path -- and most of those call sites have no business knowing about
59
+ HTTP. Left as a plain exception it reached the REST layer as an unhandled
60
+ error, so "Institution JH is not permitted for this tenant" showed up in the
61
+ UI as a bare 500. Subclassing means every path reports the registry's reason
62
+ without each one having to catch and translate.
38
63
  """
39
64
 
40
65
  def __init__(self, message, status_code=None, detail=None):
41
- super().__init__(message)
66
+ super().__init__(message, code=_rest_code(status_code))
42
67
  self.status_code = status_code
43
68
  self.detail = detail
44
69
 
@@ -82,9 +107,25 @@ class IGSNClient:
82
107
  detail = response.json().get("detail")
83
108
  except ValueError:
84
109
  detail = response.text[:500]
110
+ # The verb and path are what an operator needs, so they go to the
111
+ # log; the message itself is shown to the user verbatim for a 4xx,
112
+ # where they would only be noise.
113
+ logger.warning(
114
+ "IGSN service returned %s for %s %s: %s",
115
+ response.status_code,
116
+ method,
117
+ path,
118
+ detail,
119
+ )
120
+ if response.status_code in CLIENT_ERROR_STATUSES:
121
+ message = f"IGSN registry rejected the request: {detail}"
122
+ else:
123
+ message = (
124
+ f"IGSN service returned {response.status_code} for "
125
+ f"{method} {path}: {detail}"
126
+ )
85
127
  raise IGSNServiceError(
86
- f"IGSN service returned {response.status_code} for "
87
- f"{method} {path}: {detail}",
128
+ message,
88
129
  status_code=response.status_code,
89
130
  detail=detail,
90
131
  )
@@ -10,6 +10,13 @@ answer briefly. The local settings remain the fallback for local mode and for
10
10
  when the service is momentarily unreachable: rejecting a prefix that was valid
11
11
  five minutes ago is worse than using a slightly stale vocabulary, since the
12
12
  service validates every allocation again anyway.
13
+
14
+ The registry's vocabulary is global but allocation is tenant-scoped, so the two
15
+ are not the same list. ``GET /vocabularies`` returns every institution the
16
+ registry knows; ``GET /whoami`` says which of them this instance's tenant may
17
+ allocate under. Without the intersection the form happily offers JH to a tenant
18
+ that only holds AB, and the refusal arrives after the metadata has been filled
19
+ in -- readable now, but still a dead end.
13
20
  """
14
21
 
15
22
  import logging
@@ -43,6 +50,41 @@ def invalidate():
43
50
  _cache["value"] = None
44
51
 
45
52
 
53
+ def _permitted(institutions, client):
54
+ """Narrow the registry's institutions to the ones this tenant may use.
55
+
56
+ An empty ``allowed_institutions`` means unrestricted -- the service reads it
57
+ that way too (``validate_prefix`` only checks when the list is non-empty),
58
+ and the two must agree or the form offers less than the registry accepts.
59
+
60
+ A ``whoami`` that fails is not worth discarding a good vocabulary over: the
61
+ unfiltered list is what was shown before, and the allocation is validated
62
+ again at the service anyway.
63
+ """
64
+ try:
65
+ allowed = set((client.whoami() or {}).get("allowed_institutions") or ())
66
+ except IGSNServiceError:
67
+ logger.exception("Could not read the tenant from the IGSN service")
68
+ return institutions
69
+
70
+ if not allowed:
71
+ return institutions
72
+
73
+ permitted = {
74
+ code: value for code, value in institutions.items() if code in allowed
75
+ }
76
+ if not permitted:
77
+ # Nothing the tenant is allowed to use exists in the vocabulary, so the
78
+ # dropdown will be empty. That is the honest answer -- every prefix
79
+ # would be refused -- but it is a misconfiguration, not a normal state.
80
+ logger.warning(
81
+ "Tenant is allowed institutions %s, none of which the IGSN "
82
+ "vocabulary defines",
83
+ sorted(allowed),
84
+ )
85
+ return permitted
86
+
87
+
46
88
  def get_vocabularies():
47
89
  """Return ``{"institutions": ..., "materials": ...}``."""
48
90
  client = None
@@ -62,7 +104,7 @@ def get_vocabularies():
62
104
  try:
63
105
  remote = client.vocabularies()
64
106
  value = {
65
- "institutions": remote["institutions"],
107
+ "institutions": _permitted(remote["institutions"], client),
66
108
  "materials": remote["materials"],
67
109
  }
68
110
  except (IGSNServiceError, KeyError):
@@ -350,7 +350,11 @@ class Deposition(Resource):
350
350
  except pymongo.errors.BulkWriteError as e:
351
351
  raise RestException(f"Error creating child depositions: {e.details}")
352
352
  except IGSNServiceError as e:
353
- raise RestException(f"IGSN registry rejected the batch: {e}")
353
+ # Re-raised only to name the operation; ``e`` already carries the
354
+ # registry's reason and the status it should surface as.
355
+ raise RestException(
356
+ f"IGSN registry rejected the batch: {e.detail or e}", code=e.code
357
+ )
354
358
  return DepositionModel().load(
355
359
  result.inserted_ids[0], user=self.getCurrentUser()
356
360
  )
@@ -0,0 +1,118 @@
1
+ """Pull IGSN registry state onto local depositions.
2
+
3
+ Publication is asynchronous the whole way down: Girder queues it with the
4
+ registry, the registry queues it with DataCite, and nothing pushes the outcome
5
+ back. ``publish_deposition`` polls for a while after queuing one, which covers
6
+ the ordinary case, but not a record published straight against the registry's
7
+ API, not one that settled while Girder was down, and not one that took longer
8
+ than that poll chain was willing to wait. This is the backstop for those, and
9
+ is meant to run on a timer::
10
+
11
+ */15 * * * * girder-jsonforms-reconcile-igsn --quiet
12
+
13
+ It runs the ``reconcile`` task in-process -- no broker, no worker -- against
14
+ whatever database the local Girder configuration points at, so it is also the
15
+ quickest way to repair one record by hand.
16
+
17
+ By default it only looks at depositions that have not settled locally, which is
18
+ what a timer is for and what keeps ``--limit`` meaningful: the scan has no
19
+ ordering, so a flat limit over every deposition would re-check the same first
20
+ thousand for ever. ``--all`` does the full pass, which is the one that also
21
+ reports depositions the registry has never heard of.
22
+
23
+ Usage::
24
+
25
+ girder-jsonforms-reconcile-igsn # unsettled records
26
+ girder-jsonforms-reconcile-igsn --all # full audit, to --limit
27
+ girder-jsonforms-reconcile-igsn --prefix JHAMAL
28
+ girder-jsonforms-reconcile-igsn --igsn JHAMAL00001 --igsn JHAMAL00002
29
+ girder-jsonforms-reconcile-igsn --quiet # summary only on trouble
30
+
31
+ Exits non-zero when a record could not be read from the registry, so a cron
32
+ run that hit a broken service says so. A deposition the registry has never
33
+ heard of is logged and counted, but is not a failure: it is a finding for a
34
+ human, and reconcile deliberately does not invent the missing record.
35
+ """
36
+
37
+ import argparse
38
+ import logging
39
+ import sys
40
+
41
+ logger = logging.getLogger("girder_jsonforms.reconcile_igsn")
42
+
43
+
44
+ def main(argv=None):
45
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
46
+ parser.add_argument(
47
+ "--prefix",
48
+ help="Only reconcile depositions whose IGSN starts with this prefix.",
49
+ )
50
+ parser.add_argument(
51
+ "--igsn",
52
+ action="append",
53
+ dest="igsns",
54
+ metavar="IGSN",
55
+ help="Reconcile one IGSN. Repeatable; overrides --prefix.",
56
+ )
57
+ parser.add_argument(
58
+ "--all",
59
+ action="store_true",
60
+ dest="check_all",
61
+ help=(
62
+ "Also re-check records that have already settled locally, and "
63
+ "report any the registry does not have."
64
+ ),
65
+ )
66
+ parser.add_argument(
67
+ "--limit",
68
+ type=int,
69
+ default=1000,
70
+ help="Maximum number of depositions to check (default: 1000).",
71
+ )
72
+ parser.add_argument(
73
+ "-q",
74
+ "--quiet",
75
+ action="store_true",
76
+ help="Log warnings and errors only -- a quiet cron run stays quiet.",
77
+ )
78
+ parser.add_argument("-v", "--verbose", action="store_true", help="Debug logging.")
79
+ args = parser.parse_args(argv)
80
+
81
+ level = logging.INFO
82
+ if args.quiet:
83
+ level = logging.WARNING
84
+ if args.verbose:
85
+ level = logging.DEBUG
86
+ logging.basicConfig(level=level, format="%(levelname)s %(message)s")
87
+
88
+ # Imported here so ``--help`` works without a configured Girder/DB.
89
+ from girder_jsonforms.worker_plugin.igsn_registry import reconcile
90
+
91
+ summary = reconcile(
92
+ prefix=args.prefix,
93
+ igsns=args.igsns,
94
+ limit=args.limit,
95
+ pending_only=not args.check_all,
96
+ )
97
+ if summary is None:
98
+ # Local mode. Not an error: an instance that allocates its own IGSNs has
99
+ # no registry to reconcile against, and a cron entry left behind after
100
+ # switching modes should not start failing.
101
+ logger.warning("No IGSN registry is configured; nothing to reconcile.")
102
+ return 0
103
+
104
+ logger.info(
105
+ "Reconciled: %(updated)s updated, %(unchanged)s unchanged, "
106
+ "%(missing)s missing, %(errors)s error(s).",
107
+ summary,
108
+ )
109
+ if summary["missing"]:
110
+ logger.warning(
111
+ "%s deposition(s) are not in the registry; see the lines above.",
112
+ summary["missing"],
113
+ )
114
+ return 1 if summary["errors"] else 0
115
+
116
+
117
+ if __name__ == "__main__":
118
+ sys.exit(main())