plastron-web 4.3.2__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.
- plastron_web-4.3.2/PKG-INFO +104 -0
- plastron_web-4.3.2/README.md +87 -0
- plastron_web-4.3.2/VERSION +1 -0
- plastron_web-4.3.2/pyproject.toml +42 -0
- plastron_web-4.3.2/setup.cfg +4 -0
- plastron_web-4.3.2/src/plastron/web/__init__.py +87 -0
- plastron_web-4.3.2/src/plastron/web/activitystream.py +94 -0
- plastron_web-4.3.2/src/plastron/web/server.py +28 -0
- plastron_web-4.3.2/src/plastron_web.egg-info/PKG-INFO +104 -0
- plastron_web-4.3.2/src/plastron_web.egg-info/SOURCES.txt +14 -0
- plastron_web-4.3.2/src/plastron_web.egg-info/dependency_links.txt +1 -0
- plastron_web-4.3.2/src/plastron_web.egg-info/entry_points.txt +2 -0
- plastron_web-4.3.2/src/plastron_web.egg-info/requires.txt +10 -0
- plastron_web-4.3.2/src/plastron_web.egg-info/top_level.txt +1 -0
- plastron_web-4.3.2/tests/test_activitystream.py +228 -0
- plastron_web-4.3.2/tests/test_web.py +99 -0
|
@@ -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,87 @@
|
|
|
1
|
+
# plastron-web
|
|
2
|
+
|
|
3
|
+
HTTP server for synchronous remote operations
|
|
4
|
+
|
|
5
|
+
## Running with Python
|
|
6
|
+
|
|
7
|
+
As a Flask application:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
flask --app plastron.web:create_app("/path/to/docker-plastron.yml") run
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
To enable debugging, for hot code reloading, set `FLASK_DEBUG=1` either on
|
|
14
|
+
the command line or in a `.env` file:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
FLASK_DEBUG=1 flask --app plastron.web:create_app("/path/to/docker-plastron.yml") run
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Using the console script entrypoint, which runs the application with the
|
|
21
|
+
[Waitress] WSGI server:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
plastrond-http
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Docker Image
|
|
28
|
+
|
|
29
|
+
The plastron-stomp package contains a [Dockerfile](Dockerfile) for
|
|
30
|
+
building the `plastrond-http` Docker image.
|
|
31
|
+
|
|
32
|
+
### Building
|
|
33
|
+
|
|
34
|
+
**Important:** This image **MUST** be built from the main _plastron_
|
|
35
|
+
project directory, in order to include the other plastron packages in the
|
|
36
|
+
build context.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
docker build -t docker.lib.umd.edu/plastrond-http:latest \
|
|
40
|
+
-f plastron-web/Dockerfile .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Running with Docker Swarm
|
|
44
|
+
|
|
45
|
+
This repository contains a [compose.yml](compose.yml) file that defines
|
|
46
|
+
part of a `plastrond` Docker stack intended to be run alongside the
|
|
47
|
+
[umd-fcrepo-docker] stack. This repository's configuration adds a
|
|
48
|
+
`plastrond-http` container.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# if you are not already running in swarm mode
|
|
52
|
+
docker swarm init
|
|
53
|
+
|
|
54
|
+
# build the image
|
|
55
|
+
docker build -t docker.lib.umd.edu/plastrond-http:latest \
|
|
56
|
+
-f plastron-web/Dockerfile .
|
|
57
|
+
|
|
58
|
+
# Copy the docker-plastron-template.yml and edit the configuration
|
|
59
|
+
cp docker-plastron.template.yml docker-plastron.yml
|
|
60
|
+
vim docker-plastron.yml
|
|
61
|
+
|
|
62
|
+
# deploy the stack to run the HTTP webapp
|
|
63
|
+
docker stack deploy -c plastron-web/compose.yml plastrond
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
To watch the logs:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
docker service logs -f plastrond_http
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
To stop the HTTP service:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
docker service rm plastrond_http
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
The application is configured through environment variables.
|
|
81
|
+
|
|
82
|
+
| Name | Value | Default |
|
|
83
|
+
|------------|--------------------------------------------|---------|
|
|
84
|
+
| `JOBS_DIR` | Root directory for storing job information | `jobs` |
|
|
85
|
+
|
|
86
|
+
[umd-fcrepo-docker]: https://github.com/umd-lib/umd-fcrepo-docker
|
|
87
|
+
[Waitress]: https://pypi.org/project/waitress/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.3.2
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "plastron-web"
|
|
3
|
+
description = "Plastron HTTP web app"
|
|
4
|
+
authors = [
|
|
5
|
+
{ name='University of Maryland Libraries', email='lib-ssdr@umd.edu' },
|
|
6
|
+
{ name='Josh Westgard', email='westgard@umd.edu' },
|
|
7
|
+
{ name='Peter Eichman', email='peichman@umd.edu' },
|
|
8
|
+
{ name='Mohamed Abdul Rasheed', email='mohideen@umd.edu' },
|
|
9
|
+
{ name='Ben Wallberg', email='wallberg@umd.edu' },
|
|
10
|
+
{ name='David Steelman', email='dsteelma@umd.edu' },
|
|
11
|
+
{ name='Marc Andreu Grillo Aguilar', email='aguilarm@umd.edu' },
|
|
12
|
+
]
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
requires-python = ">= 3.8"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"click",
|
|
17
|
+
"flask",
|
|
18
|
+
"plastron-repo",
|
|
19
|
+
"plastron-utils",
|
|
20
|
+
"python-dotenv",
|
|
21
|
+
"waitress",
|
|
22
|
+
]
|
|
23
|
+
dynamic = ["version"]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
test = [
|
|
27
|
+
"pytest",
|
|
28
|
+
"pytest-cov",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
plastrond-http = 'plastron.web.server:run'
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["setuptools>=66.1.0"]
|
|
36
|
+
build-backend = "setuptools.build_meta"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.dynamic]
|
|
39
|
+
version = { "file" = "VERSION" }
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
markers = ['jobs_dir']
|
|
@@ -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
|
|
@@ -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,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
VERSION
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/plastron/web/__init__.py
|
|
5
|
+
src/plastron/web/activitystream.py
|
|
6
|
+
src/plastron/web/server.py
|
|
7
|
+
src/plastron_web.egg-info/PKG-INFO
|
|
8
|
+
src/plastron_web.egg-info/SOURCES.txt
|
|
9
|
+
src/plastron_web.egg-info/dependency_links.txt
|
|
10
|
+
src/plastron_web.egg-info/entry_points.txt
|
|
11
|
+
src/plastron_web.egg-info/requires.txt
|
|
12
|
+
src/plastron_web.egg-info/top_level.txt
|
|
13
|
+
tests/test_activitystream.py
|
|
14
|
+
tests/test_web.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
plastron
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from unittest.mock import MagicMock, ANY
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from plastron.context import PlastronContext
|
|
7
|
+
from plastron.handles import HandleServiceClient, HandleInfo
|
|
8
|
+
from plastron.repo import Repository
|
|
9
|
+
from plastron.repo.publish import PublishableResource
|
|
10
|
+
from plastron.web import create_app
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# from plastron.web.activitystream
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def app(config_file_path, request):
|
|
17
|
+
return create_app(config_file_path(request))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def app_client(app):
|
|
22
|
+
return app.test_client()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def post_data():
|
|
27
|
+
return {
|
|
28
|
+
"@context": [
|
|
29
|
+
"https://www.w3.org/ns/activitystreams",
|
|
30
|
+
{
|
|
31
|
+
"umdact": "http://vocab.lib.umd.edu/activity#",
|
|
32
|
+
"Publish": "umdact:Publish",
|
|
33
|
+
"PublishHidden": "umdact:PublishHidden",
|
|
34
|
+
"Unpublish": "umdact:Unpublish"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"type": "Publish",
|
|
38
|
+
"object": ["http://fcrepo-local:8080/fcrepo/rest/test/obj"]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.fixture
|
|
43
|
+
def request_headers():
|
|
44
|
+
mimetype = 'application/json'
|
|
45
|
+
return {
|
|
46
|
+
'Content-Type': mimetype,
|
|
47
|
+
'Accept': mimetype
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.fixture
|
|
52
|
+
def mock_resource():
|
|
53
|
+
mock_resource = MagicMock(spec=PublishableResource)
|
|
54
|
+
mock_resource.read.return_value = mock_resource
|
|
55
|
+
mock_handle = MagicMock(spec=HandleInfo)
|
|
56
|
+
mock_resource.publish.return_value = mock_handle
|
|
57
|
+
return mock_resource
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.fixture
|
|
61
|
+
def mock_context(mock_resource):
|
|
62
|
+
mock_repo = MagicMock(spec=Repository)
|
|
63
|
+
mock_repo.__getitem__.return_value = mock_resource
|
|
64
|
+
mock_context = MagicMock(spec=PlastronContext, repo=mock_repo, handle_client=MagicMock(spec=HandleServiceClient))
|
|
65
|
+
mock_context.get_public_url.return_value = 'http://digital-local/foo'
|
|
66
|
+
return mock_context
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.parametrize(
|
|
70
|
+
('input_json', 'expected_args'),
|
|
71
|
+
[
|
|
72
|
+
(
|
|
73
|
+
# json input
|
|
74
|
+
{
|
|
75
|
+
"@context": [
|
|
76
|
+
"https://www.w3.org/ns/activitystreams",
|
|
77
|
+
{
|
|
78
|
+
"umdact": "http://vocab.lib.umd.edu/activity#",
|
|
79
|
+
"Publish": "umdact:Publish",
|
|
80
|
+
"PublishHidden": "umdact:PublishHidden",
|
|
81
|
+
"Unpublish": "umdact:Unpublish"
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
"type": "Publish",
|
|
85
|
+
"object": ["http://fcrepo-local:8080/fcrepo/rest/test/obj"]
|
|
86
|
+
},
|
|
87
|
+
# expected args
|
|
88
|
+
{
|
|
89
|
+
'force_hidden': False,
|
|
90
|
+
'force_visible': False
|
|
91
|
+
},
|
|
92
|
+
),
|
|
93
|
+
(
|
|
94
|
+
# json input
|
|
95
|
+
{
|
|
96
|
+
"@context": [
|
|
97
|
+
"https://www.w3.org/ns/activitystreams",
|
|
98
|
+
{
|
|
99
|
+
"umdact": "http://vocab.lib.umd.edu/activity#",
|
|
100
|
+
"Publish": "umdact:Publish",
|
|
101
|
+
"PublishHidden": "umdact:PublishHidden",
|
|
102
|
+
"Unpublish": "umdact:Unpublish"
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
"type": "PublishHidden",
|
|
106
|
+
"object": ["http://fcrepo-local:8080/fcrepo/rest/test/obj"]
|
|
107
|
+
},
|
|
108
|
+
# expected args
|
|
109
|
+
{
|
|
110
|
+
'force_hidden': True,
|
|
111
|
+
'force_visible': False
|
|
112
|
+
},
|
|
113
|
+
),
|
|
114
|
+
],
|
|
115
|
+
)
|
|
116
|
+
def test_new_activity_publish(
|
|
117
|
+
app,
|
|
118
|
+
app_client,
|
|
119
|
+
mock_context,
|
|
120
|
+
mock_resource,
|
|
121
|
+
post_data,
|
|
122
|
+
request_headers,
|
|
123
|
+
input_json,
|
|
124
|
+
expected_args,
|
|
125
|
+
):
|
|
126
|
+
app.config['CONTEXT'] = mock_context
|
|
127
|
+
|
|
128
|
+
url = '/inbox'
|
|
129
|
+
response = app_client.post(url, data=json.dumps(input_json), headers=request_headers)
|
|
130
|
+
assert response.status_code == 201
|
|
131
|
+
|
|
132
|
+
mock_resource.publish.assert_called_once_with(handle_client=ANY, public_url=ANY, **expected_args)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@pytest.mark.parametrize(
|
|
136
|
+
('input_json', 'expected_args'),
|
|
137
|
+
[
|
|
138
|
+
(
|
|
139
|
+
# json input
|
|
140
|
+
{
|
|
141
|
+
"@context": [
|
|
142
|
+
"https://www.w3.org/ns/activitystreams",
|
|
143
|
+
{
|
|
144
|
+
"umdact": "http://vocab.lib.umd.edu/activity#",
|
|
145
|
+
"Publish": "umdact:Publish",
|
|
146
|
+
"PublishHidden": "umdact:PublishHidden",
|
|
147
|
+
"Unpublish": "umdact:Unpublish"
|
|
148
|
+
}
|
|
149
|
+
],
|
|
150
|
+
"type": "Unpublish",
|
|
151
|
+
"object": ["http://fcrepo-local:8080/fcrepo/rest/test/obj"]
|
|
152
|
+
},
|
|
153
|
+
# expected args
|
|
154
|
+
{
|
|
155
|
+
'force_hidden': False,
|
|
156
|
+
'force_visible': False
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
],
|
|
160
|
+
)
|
|
161
|
+
def test_new_activity_unpublish(
|
|
162
|
+
app,
|
|
163
|
+
app_client,
|
|
164
|
+
mock_context,
|
|
165
|
+
mock_resource,
|
|
166
|
+
post_data,
|
|
167
|
+
request_headers,
|
|
168
|
+
input_json,
|
|
169
|
+
expected_args,
|
|
170
|
+
):
|
|
171
|
+
app.config['CONTEXT'] = mock_context
|
|
172
|
+
|
|
173
|
+
url = '/inbox'
|
|
174
|
+
response = app_client.post(url, data=json.dumps(input_json), headers=request_headers)
|
|
175
|
+
assert response.status_code == 201
|
|
176
|
+
|
|
177
|
+
mock_resource.unpublish.assert_called_once_with(**expected_args)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@pytest.mark.parametrize(
|
|
181
|
+
'input_json',
|
|
182
|
+
[
|
|
183
|
+
# Missing type
|
|
184
|
+
{
|
|
185
|
+
"@context": [
|
|
186
|
+
"https://www.w3.org/ns/activitystreams",
|
|
187
|
+
{
|
|
188
|
+
"umdact": "http://vocab.lib.umd.edu/activity#",
|
|
189
|
+
"Publish": "umdact:Publish",
|
|
190
|
+
"PublishHidden": "umdact:PublishHidden",
|
|
191
|
+
"Unpublish": "umdact:Unpublish"
|
|
192
|
+
}
|
|
193
|
+
],
|
|
194
|
+
"object": ["http://fcrepo-local:8080/fcrepo/rest/test/obj"]
|
|
195
|
+
},
|
|
196
|
+
# Invalid type
|
|
197
|
+
{
|
|
198
|
+
"@context": [
|
|
199
|
+
"https://www.w3.org/ns/activitystreams",
|
|
200
|
+
{
|
|
201
|
+
"umdact": "http://vocab.lib.umd.edu/activity#",
|
|
202
|
+
"Publish": "umdact:Publish",
|
|
203
|
+
"PublishHidden": "umdact:PublishHidden",
|
|
204
|
+
"Unpublish": "umdact:Unpublish"
|
|
205
|
+
}
|
|
206
|
+
],
|
|
207
|
+
"type": "foo",
|
|
208
|
+
"object": ["http://fcrepo-local:8080/fcrepo/rest/test/obj"]
|
|
209
|
+
},
|
|
210
|
+
# Missing target object
|
|
211
|
+
{
|
|
212
|
+
"@context": [
|
|
213
|
+
"https://www.w3.org/ns/activitystreams",
|
|
214
|
+
{
|
|
215
|
+
"umdact": "http://vocab.lib.umd.edu/activity#",
|
|
216
|
+
"Publish": "umdact:Publish",
|
|
217
|
+
"PublishHidden": "umdact:PublishHidden",
|
|
218
|
+
"Unpublish": "umdact:Unpublish"
|
|
219
|
+
}
|
|
220
|
+
],
|
|
221
|
+
"type": "Publish",
|
|
222
|
+
}
|
|
223
|
+
],
|
|
224
|
+
)
|
|
225
|
+
def test_new_activity_invalid_input(app_client, post_data, request_headers, input_json):
|
|
226
|
+
url = '/inbox'
|
|
227
|
+
response = app_client.post(url, data=json.dumps(input_json), headers=request_headers)
|
|
228
|
+
assert response.status_code == 400
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from plastron.web import create_app
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.fixture
|
|
7
|
+
def app_client(config_file_path, datadir, monkeypatch, request):
|
|
8
|
+
def _create_app_client(jobs_dir):
|
|
9
|
+
test_jobs_dir = datadir / jobs_dir
|
|
10
|
+
monkeypatch.setenv('JOBS_DIR', str(test_jobs_dir))
|
|
11
|
+
app = create_app(config_file_path(request))
|
|
12
|
+
|
|
13
|
+
return app.test_client()
|
|
14
|
+
|
|
15
|
+
return _create_app_client
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_no_jobs(app_client):
|
|
19
|
+
response = app_client('jobsempty').get('/jobs')
|
|
20
|
+
assert response.status_code == 200
|
|
21
|
+
data = response.get_json()
|
|
22
|
+
assert 'jobs' in data
|
|
23
|
+
assert len(data['jobs']) == 0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_jobs_found(app_client):
|
|
27
|
+
response = app_client('jobs').get('/jobs')
|
|
28
|
+
assert response.status_code == 200
|
|
29
|
+
data = response.get_json()
|
|
30
|
+
assert 'jobs' in data
|
|
31
|
+
assert len(data['jobs']) == 5
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_job_not_found(app_client):
|
|
35
|
+
response = app_client('jobs').get('/jobs/FOOBAR')
|
|
36
|
+
assert response.status_code == 404
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_empty_job_dir(app_client):
|
|
40
|
+
response = app_client('jobs').get('/jobs/noconfigfile')
|
|
41
|
+
assert response.status_code == 404
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_empty_config_file(app_client):
|
|
45
|
+
response = app_client('jobs').get('/jobs/emptyconfigfile')
|
|
46
|
+
assert response.status_code == 404
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_valid_config_file_no_metadata(app_client):
|
|
50
|
+
response = app_client('jobs').get('/jobs/nometadata')
|
|
51
|
+
assert response.status_code == 404
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_valid_job(app_client):
|
|
55
|
+
response = app_client('jobs').get('/jobs/validjob')
|
|
56
|
+
assert response.status_code == 200
|
|
57
|
+
data = response.get_json()
|
|
58
|
+
assert '@id' in data
|
|
59
|
+
assert 'completed' in data
|
|
60
|
+
assert data['completed']['count'] == 0
|
|
61
|
+
assert len(data['completed']['items']) == 0
|
|
62
|
+
assert 'dropped' in data
|
|
63
|
+
assert len(data['dropped']) == 0
|
|
64
|
+
assert 'runs' in data
|
|
65
|
+
assert len(data['runs']) == 0
|
|
66
|
+
assert data['access'] is None
|
|
67
|
+
assert data['binaries_location'] == 'data'
|
|
68
|
+
assert data['container'] == '/dc/2021/2'
|
|
69
|
+
assert data['job_id'] == 'import-20210505143008'
|
|
70
|
+
assert data['member_of'] == 'https://fcrepo.lib.umd.edu/fcrepo/rest/dc/2021/2'
|
|
71
|
+
assert data['model'] == 'Item'
|
|
72
|
+
assert data['total'] == 9
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_valid_completed_job(app_client):
|
|
76
|
+
response = app_client('jobs').get('/jobs/validcompletedjob')
|
|
77
|
+
assert response.status_code == 200
|
|
78
|
+
data = response.get_json()
|
|
79
|
+
assert '@id' in data
|
|
80
|
+
assert 'completed' in data
|
|
81
|
+
assert data['completed']['count'] == 9
|
|
82
|
+
assert len(data['completed']['items']) == 9
|
|
83
|
+
assert 'dropped' in data
|
|
84
|
+
# 3 keys: "failed", "invalid", "timestamp"
|
|
85
|
+
assert len(data['dropped']) == 3
|
|
86
|
+
assert 'failed' in data['dropped']
|
|
87
|
+
assert 'invalid' in data['dropped']
|
|
88
|
+
assert 'timestamp' in data['dropped']
|
|
89
|
+
assert data['dropped']['timestamp'] == '20210505143008'
|
|
90
|
+
assert 'runs' in data
|
|
91
|
+
assert len(data['runs']) == 1
|
|
92
|
+
assert data['runs'][0] == '20210505143008'
|
|
93
|
+
assert data['access'] is None
|
|
94
|
+
assert data['binaries_location'] == 'data'
|
|
95
|
+
assert data['container'] == '/dc/2021/2'
|
|
96
|
+
assert data['job_id'] == 'import-20210505143008'
|
|
97
|
+
assert data['member_of'] == 'https://fcrepo.lib.umd.edu/fcrepo/rest/dc/2021/2'
|
|
98
|
+
assert data['model'] == 'Item'
|
|
99
|
+
assert data['total'] == 9
|