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.
- plastron/web/__init__.py +87 -0
- plastron/web/activitystream.py +94 -0
- plastron/web/server.py +28 -0
- plastron_web-4.3.2.dist-info/METADATA +104 -0
- plastron_web-4.3.2.dist-info/RECORD +8 -0
- plastron_web-4.3.2.dist-info/WHEEL +5 -0
- plastron_web-4.3.2.dist-info/entry_points.txt +2 -0
- plastron_web-4.3.2.dist-info/top_level.txt +1 -0
plastron/web/__init__.py
ADDED
|
@@ -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 @@
|
|
|
1
|
+
plastron
|