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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,2 @@
1
+ [console_scripts]
2
+ plastrond-http = plastron.web.server:run
@@ -0,0 +1,10 @@
1
+ click
2
+ flask
3
+ plastron-repo
4
+ plastron-utils
5
+ python-dotenv
6
+ waitress
7
+
8
+ [test]
9
+ pytest
10
+ pytest-cov
@@ -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