plastron-web 4.3.2__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,87 @@
1
+ import logging
2
+ import os
3
+ import yaml
4
+ import urllib.parse
5
+ from pathlib import Path
6
+ from argparse import Namespace
7
+ from flask import Flask, url_for
8
+ from werkzeug.exceptions import NotFound
9
+
10
+ from plastron.context import PlastronContext
11
+ from plastron.jobs.importjob import ImportJob
12
+ from plastron.jobs import JobError, JobConfigError, JobNotFoundError, Jobs
13
+ from plastron.web.activitystream import activitystream_bp
14
+ from plastron.utils import envsubst
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def job_url(job_id):
20
+ return url_for('show_job', _external=True, job_id=job_id)
21
+
22
+
23
+ def items(log):
24
+ return {
25
+ 'count': len(log),
26
+ 'items': [c for c in log]
27
+ }
28
+
29
+
30
+ def latest_dropped_items(job: ImportJob):
31
+ latest_run = job.latest_run()
32
+ if latest_run is None:
33
+ return {}
34
+
35
+ return {
36
+ 'timestamp': latest_run.timestamp,
37
+ 'failed': items(latest_run.failed_items),
38
+ 'invalid': items(latest_run.invalid_items)
39
+ }
40
+
41
+
42
+ def create_app(config_file: str):
43
+ app = Flask(__name__)
44
+ with open(config_file, "r") as stream:
45
+ config = envsubst(yaml.safe_load(stream))
46
+ app.config['CONTEXT'] = PlastronContext(config=config, args=Namespace(delegated_user=None))
47
+ jobs_dir = Path(os.environ.get('JOBS_DIR', 'jobs'))
48
+ jobs = Jobs(directory=jobs_dir)
49
+ app.register_blueprint(activitystream_bp)
50
+
51
+ def get_job(job_id: str):
52
+ return jobs.get_job(ImportJob, urllib.parse.unquote(job_id))
53
+
54
+ @app.route('/jobs')
55
+ def list_jobs():
56
+ if not jobs_dir.exists():
57
+ logger.warning(f'Jobs directory "{jobs_dir.absolute()}" does not exist; returning empty list')
58
+ return {'jobs': []}
59
+ job_ids = sorted(f.name for f in jobs_dir.iterdir() if f.is_dir())
60
+ return {'jobs': [{'@id': job_url(job_id), 'job_id': job_id} for job_id in job_ids]}
61
+
62
+ @app.route('/jobs/<path:job_id>')
63
+ def show_job(job_id):
64
+ try:
65
+ job = get_job(job_id)
66
+ job.load_config()
67
+ except JobNotFoundError:
68
+ logger.warning(f'Job {job_id} not found')
69
+ raise NotFound
70
+ except JobConfigError:
71
+ logger.warning(f'Cannot open config file for job {job_id}')
72
+ # TODO: more complete information in the response body?
73
+ raise NotFound
74
+
75
+ try:
76
+ return {
77
+ '@id': job_url(job_id),
78
+ **vars(job.config),
79
+ 'runs': job.runs,
80
+ 'completed': items(job.completed_log),
81
+ 'dropped': latest_dropped_items(job),
82
+ 'total': job.get_metadata().total
83
+ }
84
+ except JobError as e:
85
+ raise NotFound from e
86
+
87
+ return app
@@ -0,0 +1,94 @@
1
+ import json
2
+ import logging
3
+ from typing import List
4
+ from uuid import uuid4
5
+
6
+ from flask import Blueprint, current_app, jsonify, request
7
+ from rdflib import Graph
8
+
9
+ from plastron.namespaces import activitystreams, rdf, umdact
10
+ from plastron.repo.publish import PublishableResource
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ activitystream_bp = Blueprint('activitystream', __name__, template_folder='templates')
15
+
16
+
17
+ @activitystream_bp.route('/inbox', methods=['POST'])
18
+ def new_activity():
19
+ try:
20
+ activity = Activity(from_json=request.get_json())
21
+ ctx = current_app.config['CONTEXT']
22
+ for uri in activity.objects:
23
+ resource: PublishableResource = ctx.repo[uri:PublishableResource].read()
24
+ if activity.publish:
25
+ resource.publish(
26
+ handle_client=ctx.handle_client,
27
+ public_url=ctx.get_public_url(resource),
28
+ force_hidden=activity.force_hidden,
29
+ force_visible=False,
30
+ )
31
+ elif activity.unpublish:
32
+ resource.unpublish(
33
+ force_hidden=activity.force_hidden,
34
+ force_visible=False,
35
+ )
36
+ return {}, 201
37
+ except ValidationError as e:
38
+ logger.error(f'Exception: {e}')
39
+ return jsonify({'error': str(e)}), 400
40
+ except Exception as e:
41
+ logger.error(f'Exception: {e}')
42
+ return jsonify({'error': str(e)}), 500
43
+
44
+
45
+ class Activity:
46
+ def __init__(self, from_json: str):
47
+ self.id = uuid4()
48
+ self._objects = []
49
+ self._publish = False
50
+ self._unpublish = False
51
+ self._force_hidden = False
52
+ g = Graph()
53
+ g.parse(data=json.dumps(from_json), format='json-ld')
54
+
55
+ for s, p, o in g:
56
+ if activitystreams.object == p:
57
+ self._objects.append(str(o))
58
+ elif rdf.type == p:
59
+ if o == umdact.Publish:
60
+ self._publish = True
61
+ elif o == umdact.Unpublish:
62
+ self._unpublish = True
63
+ elif o == umdact.PublishHidden:
64
+ self._publish = True
65
+ self._force_hidden = True
66
+ else:
67
+ raise ValidationError(f'Invalid Activity type: {str(o)}')
68
+ if not self.publish and not self.unpublish:
69
+ raise ValidationError(f'Invalid JSON-LD provided: Type not specified.')
70
+ if not self.objects:
71
+ raise ValidationError(f'Invalid JSON-LD provided: Object(s) not specified.')
72
+
73
+ def __str__(self):
74
+ return self.id
75
+
76
+ @property
77
+ def publish(self) -> bool:
78
+ return self._publish
79
+
80
+ @property
81
+ def unpublish(self) -> bool:
82
+ return self._unpublish
83
+
84
+ @property
85
+ def force_hidden(self) -> bool:
86
+ return self._force_hidden
87
+
88
+ @property
89
+ def objects(self) -> List[str]:
90
+ return self._objects
91
+
92
+
93
+ class ValidationError(Exception):
94
+ pass
plastron/web/server.py ADDED
@@ -0,0 +1,28 @@
1
+ import click
2
+ from dotenv import load_dotenv
3
+ from waitress import serve
4
+
5
+ from plastron.web import create_app
6
+
7
+
8
+ @click.command()
9
+ @click.option(
10
+ '--listen',
11
+ default='0.0.0.0:5000',
12
+ help='Address and port to listen on. Default is "0.0.0.0:5000".',
13
+ metavar='[ADDRESS]:PORT',
14
+ )
15
+ @click.option(
16
+ '-c', '--config-file',
17
+ type=click.Path(exists=True),
18
+ help='Configuration file',
19
+ required=True,
20
+ )
21
+ def run(listen: bool, config_file: str):
22
+ load_dotenv()
23
+ app = create_app(config_file)
24
+ serve(app, listen=listen)
25
+
26
+
27
+ if __name__ == "__main__":
28
+ run()
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.1
2
+ Name: plastron-web
3
+ Version: 4.3.2
4
+ Summary: Plastron HTTP web app
5
+ Author-email: University of Maryland Libraries <lib-ssdr@umd.edu>, Josh Westgard <westgard@umd.edu>, Peter Eichman <peichman@umd.edu>, Mohamed Abdul Rasheed <mohideen@umd.edu>, Ben Wallberg <wallberg@umd.edu>, David Steelman <dsteelma@umd.edu>, Marc Andreu Grillo Aguilar <aguilarm@umd.edu>
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: click
9
+ Requires-Dist: flask
10
+ Requires-Dist: plastron-repo
11
+ Requires-Dist: plastron-utils
12
+ Requires-Dist: python-dotenv
13
+ Requires-Dist: waitress
14
+ Provides-Extra: test
15
+ Requires-Dist: pytest ; extra == 'test'
16
+ Requires-Dist: pytest-cov ; extra == 'test'
17
+
18
+ # plastron-web
19
+
20
+ HTTP server for synchronous remote operations
21
+
22
+ ## Running with Python
23
+
24
+ As a Flask application:
25
+
26
+ ```bash
27
+ flask --app plastron.web:create_app("/path/to/docker-plastron.yml") run
28
+ ```
29
+
30
+ To enable debugging, for hot code reloading, set `FLASK_DEBUG=1` either on
31
+ the command line or in a `.env` file:
32
+
33
+ ```bash
34
+ FLASK_DEBUG=1 flask --app plastron.web:create_app("/path/to/docker-plastron.yml") run
35
+ ```
36
+
37
+ Using the console script entrypoint, which runs the application with the
38
+ [Waitress] WSGI server:
39
+
40
+ ```bash
41
+ plastrond-http
42
+ ```
43
+
44
+ ## Docker Image
45
+
46
+ The plastron-stomp package contains a [Dockerfile](Dockerfile) for
47
+ building the `plastrond-http` Docker image.
48
+
49
+ ### Building
50
+
51
+ **Important:** This image **MUST** be built from the main _plastron_
52
+ project directory, in order to include the other plastron packages in the
53
+ build context.
54
+
55
+ ```bash
56
+ docker build -t docker.lib.umd.edu/plastrond-http:latest \
57
+ -f plastron-web/Dockerfile .
58
+ ```
59
+
60
+ ### Running with Docker Swarm
61
+
62
+ This repository contains a [compose.yml](compose.yml) file that defines
63
+ part of a `plastrond` Docker stack intended to be run alongside the
64
+ [umd-fcrepo-docker] stack. This repository's configuration adds a
65
+ `plastrond-http` container.
66
+
67
+ ```bash
68
+ # if you are not already running in swarm mode
69
+ docker swarm init
70
+
71
+ # build the image
72
+ docker build -t docker.lib.umd.edu/plastrond-http:latest \
73
+ -f plastron-web/Dockerfile .
74
+
75
+ # Copy the docker-plastron-template.yml and edit the configuration
76
+ cp docker-plastron.template.yml docker-plastron.yml
77
+ vim docker-plastron.yml
78
+
79
+ # deploy the stack to run the HTTP webapp
80
+ docker stack deploy -c plastron-web/compose.yml plastrond
81
+ ```
82
+
83
+ To watch the logs:
84
+
85
+ ```bash
86
+ docker service logs -f plastrond_http
87
+ ```
88
+
89
+ To stop the HTTP service:
90
+
91
+ ```bash
92
+ docker service rm plastrond_http
93
+ ```
94
+
95
+ ## Configuration
96
+
97
+ The application is configured through environment variables.
98
+
99
+ | Name | Value | Default |
100
+ |------------|--------------------------------------------|---------|
101
+ | `JOBS_DIR` | Root directory for storing job information | `jobs` |
102
+
103
+ [umd-fcrepo-docker]: https://github.com/umd-lib/umd-fcrepo-docker
104
+ [Waitress]: https://pypi.org/project/waitress/
@@ -0,0 +1,8 @@
1
+ plastron/web/__init__.py,sha256=yDyDciDaSibjb2VlloOrZ51zQZ-_uqCzmue91l-7E70,2687
2
+ plastron/web/activitystream.py,sha256=Fsi563cKd3XWGTZ8TZ-W1jyyuTUSinlZJva-bMZJrd4,2956
3
+ plastron/web/server.py,sha256=UXSNsc35pHnvHs4BvFW5-2Y4EFgDlOppPhahkP66A5c,578
4
+ plastron_web-4.3.2.dist-info/METADATA,sha256=gUbvvW9f3IXiIoW5ZiJUp95qLTqZBtKBg6ZgkSheHDs,2896
5
+ plastron_web-4.3.2.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
6
+ plastron_web-4.3.2.dist-info/entry_points.txt,sha256=joOgIG9x7DNrL9ngBrJEI6aa8EtqCYS5Nv5wHLvcCFw,59
7
+ plastron_web-4.3.2.dist-info/top_level.txt,sha256=N9Rg78jS1475MaY-sd8HcULh6H1BwDebrZ50iTzBLmI,9
8
+ plastron_web-4.3.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (72.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ plastrond-http = plastron.web.server:run
@@ -0,0 +1 @@
1
+ plastron