nthp_api 0.1.9__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.
- nthp_api-0.1.9/LICENCE +20 -0
- nthp_api-0.1.9/PKG-INFO +82 -0
- nthp_api-0.1.9/README.md +49 -0
- nthp_api-0.1.9/nthp_api/__init__.py +0 -0
- nthp_api-0.1.9/nthp_api/cli/__init__.py +88 -0
- nthp_api-0.1.9/nthp_api/cli/logs.py +20 -0
- nthp_api-0.1.9/nthp_api/nthp_build/__init__.py +0 -0
- nthp_api-0.1.9/nthp_api/nthp_build/assets.py +229 -0
- nthp_api-0.1.9/nthp_api/nthp_build/config.py +22 -0
- nthp_api-0.1.9/nthp_api/nthp_build/content.py +39 -0
- nthp_api-0.1.9/nthp_api/nthp_build/database.py +142 -0
- nthp_api-0.1.9/nthp_api/nthp_build/documents.py +55 -0
- nthp_api-0.1.9/nthp_api/nthp_build/dumper.py +385 -0
- nthp_api-0.1.9/nthp_api/nthp_build/fields.py +15 -0
- nthp_api-0.1.9/nthp_api/nthp_build/history.py +17 -0
- nthp_api-0.1.9/nthp_api/nthp_build/loader.py +280 -0
- nthp_api-0.1.9/nthp_api/nthp_build/models.py +200 -0
- nthp_api-0.1.9/nthp_api/nthp_build/parallel.py +57 -0
- nthp_api-0.1.9/nthp_api/nthp_build/people.py +227 -0
- nthp_api-0.1.9/nthp_api/nthp_build/playwrights.py +137 -0
- nthp_api-0.1.9/nthp_api/nthp_build/roles.py +213 -0
- nthp_api-0.1.9/nthp_api/nthp_build/schema.py +440 -0
- nthp_api-0.1.9/nthp_api/nthp_build/search.py +17 -0
- nthp_api-0.1.9/nthp_api/nthp_build/shows.py +182 -0
- nthp_api-0.1.9/nthp_api/nthp_build/smugmug.py +40 -0
- nthp_api-0.1.9/nthp_api/nthp_build/spec.py +298 -0
- nthp_api-0.1.9/nthp_api/nthp_build/trivia.py +65 -0
- nthp_api-0.1.9/nthp_api/nthp_build/venues.py +69 -0
- nthp_api-0.1.9/nthp_api/nthp_build/version.py +8 -0
- nthp_api-0.1.9/nthp_api/nthp_build/years.py +40 -0
- nthp_api-0.1.9/nthp_api/smugmugger/__init__.py +12 -0
- nthp_api-0.1.9/nthp_api/smugmugger/album.py +20 -0
- nthp_api-0.1.9/nthp_api/smugmugger/client.py +118 -0
- nthp_api-0.1.9/nthp_api/smugmugger/config.py +16 -0
- nthp_api-0.1.9/nthp_api/smugmugger/database.py +33 -0
- nthp_api-0.1.9/nthp_api/smugmugger/schema.py +58 -0
- nthp_api-0.1.9/nthp_api/smugmugger/smugmug.py +67 -0
- nthp_api-0.1.9/pyproject.toml +92 -0
nthp_api-0.1.9/LICENCE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
Copyright (c) 2021 The Nottingham New Theatre
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
5
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
6
|
+
the Software without restriction, including without limitation the rights to
|
|
7
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
8
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
9
|
+
so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
nthp_api-0.1.9/PKG-INFO
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nthp_api
|
|
3
|
+
Version: 0.1.9
|
|
4
|
+
Summary: This is a CLI tool for building the Nottingham New Theatre's History Project content database.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENCE
|
|
7
|
+
Author: Will Pimblett
|
|
8
|
+
Author-email: will@wjdp.uk
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Dist: Markdown (>=3.4.3,<4.0.0)
|
|
18
|
+
Requires-Dist: PyYAML (>=6.0,<7.0)
|
|
19
|
+
Requires-Dist: click (>=8.1.3,<9.0.0)
|
|
20
|
+
Requires-Dist: coloredlogs (>=15.0.1,<16.0.0)
|
|
21
|
+
Requires-Dist: httpx (>=0.24.1,<0.25.0)
|
|
22
|
+
Requires-Dist: peewee (>=3.16.2,<4.0.0)
|
|
23
|
+
Requires-Dist: pydantic (>=2.1.1,<3.0.0)
|
|
24
|
+
Requires-Dist: pydantic-collections (>=0.5.0,<0.6.0)
|
|
25
|
+
Requires-Dist: pydantic-settings (>=2.0.2,<3.0.0)
|
|
26
|
+
Requires-Dist: pyhumps (>=3.8.0,<4.0.0)
|
|
27
|
+
Requires-Dist: python-frontmatter (>=1.0.0,<2.0.0)
|
|
28
|
+
Requires-Dist: python-slugify (>=8.0.1,<9.0.0)
|
|
29
|
+
Project-URL: Homepage, https://github.com/newtheatre/nthp-api
|
|
30
|
+
Project-URL: Repository, https://github.com/newtheatre/nthp-api
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# History Project API Generator
|
|
34
|
+
|
|
35
|
+
This project generates a JSON API from the [history-project](https://github.com/newtheatre/history-project) content repository. It does that in two steps:
|
|
36
|
+
|
|
37
|
+
- **load**: Generate a sqlite database from the source files.
|
|
38
|
+
- **dump**: Use that database to generate a JSON API.
|
|
39
|
+
|
|
40
|
+
## Endpoints
|
|
41
|
+
|
|
42
|
+
- The API is currently hosted at <https://nthp-api.wjdp.uk/v1>.
|
|
43
|
+
- The specification is available at <https://nthp-api.wjdp.uk/v1/master/openapi.json>.
|
|
44
|
+
- To render the spec into human-readable docs use a tool such as:
|
|
45
|
+
- ReDoc <https://redocly.github.io/redoc/?url=https://nthp-api.wjdp.uk/v1/master/openapi.json>.
|
|
46
|
+
- Stoplight <https://elements-demo.stoplight.io/?spec=https://nthp-api.wjdp.uk/v1/master/openapi.json>
|
|
47
|
+
|
|
48
|
+
# Usage
|
|
49
|
+
|
|
50
|
+
## From source
|
|
51
|
+
|
|
52
|
+
- Ensure you have [Poetry](https://python-poetry.org/) installed.
|
|
53
|
+
- Run `poetry install`.
|
|
54
|
+
- Clone the history project repository into the `content/` directory. You can do a shallow clone for this: `GIT_LFS_SKIP_SMUDGE=1 git clone --depth 1 --branch master git@github.com:newtheatre/history-project content`.
|
|
55
|
+
- Run `./nthp load` to generate the sqlite database from the content files.
|
|
56
|
+
- Run `./nthp dump` to generate the API from the database into the `dist/` directory.
|
|
57
|
+
|
|
58
|
+
## From PyPi
|
|
59
|
+
|
|
60
|
+
- With pip installed run `pip install --user nthp-api`. If you have your local bin folder on your path you should now be able to run `nthp`.
|
|
61
|
+
- Within the history-project repo run `nthp load` to generate the sqlite database from the content files.
|
|
62
|
+
- Run `nthp dump` to generate the API from the database into the `dist/` directory.
|
|
63
|
+
|
|
64
|
+
Alternatively you can run `nthp build` to run both steps in one go.
|
|
65
|
+
|
|
66
|
+
## Contributing
|
|
67
|
+
|
|
68
|
+
### pre-commit hooks
|
|
69
|
+
|
|
70
|
+
pre-commit hooks are used to lint the source code using [Black](https://black.readthedocs.io/en/stable/) and [ruff](https://ruff.rs)
|
|
71
|
+
|
|
72
|
+
- Ensure you have [pre-commit](https://pre-commit.com/) installed.
|
|
73
|
+
- Run `pre-commit install` to install pre-commit hooks.
|
|
74
|
+
|
|
75
|
+
### Tests
|
|
76
|
+
|
|
77
|
+
Run `pytest` or use the included PyCharm run configuration.
|
|
78
|
+
|
|
79
|
+
## Release
|
|
80
|
+
|
|
81
|
+
See the `bin/release.sh` script for the release process. This assumes that your local machine has the correct credentials to publish to PyPi.
|
|
82
|
+
|
nthp_api-0.1.9/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# History Project API Generator
|
|
2
|
+
|
|
3
|
+
This project generates a JSON API from the [history-project](https://github.com/newtheatre/history-project) content repository. It does that in two steps:
|
|
4
|
+
|
|
5
|
+
- **load**: Generate a sqlite database from the source files.
|
|
6
|
+
- **dump**: Use that database to generate a JSON API.
|
|
7
|
+
|
|
8
|
+
## Endpoints
|
|
9
|
+
|
|
10
|
+
- The API is currently hosted at <https://nthp-api.wjdp.uk/v1>.
|
|
11
|
+
- The specification is available at <https://nthp-api.wjdp.uk/v1/master/openapi.json>.
|
|
12
|
+
- To render the spec into human-readable docs use a tool such as:
|
|
13
|
+
- ReDoc <https://redocly.github.io/redoc/?url=https://nthp-api.wjdp.uk/v1/master/openapi.json>.
|
|
14
|
+
- Stoplight <https://elements-demo.stoplight.io/?spec=https://nthp-api.wjdp.uk/v1/master/openapi.json>
|
|
15
|
+
|
|
16
|
+
# Usage
|
|
17
|
+
|
|
18
|
+
## From source
|
|
19
|
+
|
|
20
|
+
- Ensure you have [Poetry](https://python-poetry.org/) installed.
|
|
21
|
+
- Run `poetry install`.
|
|
22
|
+
- Clone the history project repository into the `content/` directory. You can do a shallow clone for this: `GIT_LFS_SKIP_SMUDGE=1 git clone --depth 1 --branch master git@github.com:newtheatre/history-project content`.
|
|
23
|
+
- Run `./nthp load` to generate the sqlite database from the content files.
|
|
24
|
+
- Run `./nthp dump` to generate the API from the database into the `dist/` directory.
|
|
25
|
+
|
|
26
|
+
## From PyPi
|
|
27
|
+
|
|
28
|
+
- With pip installed run `pip install --user nthp-api`. If you have your local bin folder on your path you should now be able to run `nthp`.
|
|
29
|
+
- Within the history-project repo run `nthp load` to generate the sqlite database from the content files.
|
|
30
|
+
- Run `nthp dump` to generate the API from the database into the `dist/` directory.
|
|
31
|
+
|
|
32
|
+
Alternatively you can run `nthp build` to run both steps in one go.
|
|
33
|
+
|
|
34
|
+
## Contributing
|
|
35
|
+
|
|
36
|
+
### pre-commit hooks
|
|
37
|
+
|
|
38
|
+
pre-commit hooks are used to lint the source code using [Black](https://black.readthedocs.io/en/stable/) and [ruff](https://ruff.rs)
|
|
39
|
+
|
|
40
|
+
- Ensure you have [pre-commit](https://pre-commit.com/) installed.
|
|
41
|
+
- Run `pre-commit install` to install pre-commit hooks.
|
|
42
|
+
|
|
43
|
+
### Tests
|
|
44
|
+
|
|
45
|
+
Run `pytest` or use the included PyCharm run configuration.
|
|
46
|
+
|
|
47
|
+
## Release
|
|
48
|
+
|
|
49
|
+
See the `bin/release.sh` script for the release process. This assumes that your local machine has the correct credentials to publish to PyPi.
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from os import environ
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from nthp_api.cli import logs
|
|
7
|
+
from nthp_api.nthp_build.version import get_version
|
|
8
|
+
|
|
9
|
+
logs.init()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group()
|
|
16
|
+
def cli():
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@cli.command()
|
|
21
|
+
def version():
|
|
22
|
+
print(f"nthp-api {get_version()}") # noqa T201
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@click.argument("path", type=click.Path(exists=True))
|
|
26
|
+
@cli.command()
|
|
27
|
+
def load(path):
|
|
28
|
+
environ["CONTENT_ROOT"] = str(path)
|
|
29
|
+
|
|
30
|
+
from nthp_api.nthp_build import database, loader
|
|
31
|
+
|
|
32
|
+
database.init_db(create=True)
|
|
33
|
+
loader.run_loaders()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@cli.command()
|
|
37
|
+
def stats():
|
|
38
|
+
environ["CONTENT_ROOT"] = "does-not-matter"
|
|
39
|
+
|
|
40
|
+
from nthp_api.nthp_build import database
|
|
41
|
+
|
|
42
|
+
database.init_db()
|
|
43
|
+
database.show_stats()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@cli.command()
|
|
47
|
+
def smug():
|
|
48
|
+
environ["CONTENT_ROOT"] = "does-not-matter"
|
|
49
|
+
|
|
50
|
+
import nthp_api.smugmugger.database
|
|
51
|
+
from nthp_api.nthp_build import database, smugmug
|
|
52
|
+
|
|
53
|
+
database.init_db()
|
|
54
|
+
nthp_api.smugmugger.database.init_db()
|
|
55
|
+
smugmug.run()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@cli.command()
|
|
59
|
+
def dump():
|
|
60
|
+
environ["CONTENT_ROOT"] = "does-not-matter"
|
|
61
|
+
|
|
62
|
+
from nthp_api.nthp_build import database, dumper
|
|
63
|
+
|
|
64
|
+
database.init_db()
|
|
65
|
+
dumper.delete_output_dir()
|
|
66
|
+
dumper.dump_all()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@click.argument("path", type=click.Path(exists=True))
|
|
70
|
+
@cli.command()
|
|
71
|
+
def build(path):
|
|
72
|
+
# Set settings using environment variables as workers and threads will recreate
|
|
73
|
+
# the settings object and not pick up the values if set here.
|
|
74
|
+
environ["DB_URI"] = ":memory:"
|
|
75
|
+
environ["CONTENT_ROOT"] = str(path)
|
|
76
|
+
|
|
77
|
+
log.info(f"Building from {path} using in-memory database")
|
|
78
|
+
|
|
79
|
+
import nthp_api.smugmugger.database
|
|
80
|
+
from nthp_api.nthp_build import database, dumper, loader, smugmug
|
|
81
|
+
|
|
82
|
+
database.init_db(create=True)
|
|
83
|
+
loader.run_loaders()
|
|
84
|
+
database.show_stats()
|
|
85
|
+
nthp_api.smugmugger.database.init_db()
|
|
86
|
+
smugmug.run()
|
|
87
|
+
dumper.delete_output_dir()
|
|
88
|
+
dumper.dump_all()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from os import environ
|
|
3
|
+
|
|
4
|
+
import coloredlogs
|
|
5
|
+
|
|
6
|
+
LOG_FORMAT = "%(asctime)s %(levelname)s %(message)s"
|
|
7
|
+
DATE_FORMAT = "%H:%M:%S"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def init():
|
|
11
|
+
nthp_build_logger = logging.getLogger("nthp_api.nthp_build")
|
|
12
|
+
smugmugger_logger = logging.getLogger("nthp_api.smugmugger")
|
|
13
|
+
|
|
14
|
+
log_level = environ.get("LOG_LEVEL", "INFO")
|
|
15
|
+
coloredlogs.install(
|
|
16
|
+
level=log_level, logger=nthp_build_logger, fmt=LOG_FORMAT, datefmt=DATE_FORMAT
|
|
17
|
+
)
|
|
18
|
+
coloredlogs.install(
|
|
19
|
+
level=log_level, logger=smugmugger_logger, fmt=LOG_FORMAT, datefmt=DATE_FORMAT
|
|
20
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import mimetypes
|
|
4
|
+
from collections.abc import Generator, Iterable
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
from nthp_api.nthp_build import database, models, schema
|
|
8
|
+
from nthp_api.nthp_build.documents import DocumentPath
|
|
9
|
+
from nthp_api.smugmugger import SmugMugImage
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AssetTarget(database.DbCompatEnumMixin, Enum):
|
|
15
|
+
SHOW = "show"
|
|
16
|
+
PERSON = "person"
|
|
17
|
+
VENUE = "venue"
|
|
18
|
+
ALBUM = "album"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AssetSource(database.DbCompatEnumMixin, Enum):
|
|
22
|
+
SMUGMUG = "smugmug"
|
|
23
|
+
FILE = "file"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AssetType(database.DbCompatEnumMixin, Enum):
|
|
27
|
+
ALBUM = "album" # A collection of other assets
|
|
28
|
+
IMAGE = "image" # A single image, a photo or a scanned document
|
|
29
|
+
VIDEO = "video" # A video
|
|
30
|
+
OTHER = "other" # A video
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AssetCategory(database.DbCompatEnumMixin, Enum):
|
|
34
|
+
POSTER = "poster"
|
|
35
|
+
FLYER = "flyer"
|
|
36
|
+
PROGRAMME = "programme"
|
|
37
|
+
HEADSHOT = "headshot"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_mime_type(source: AssetSource, type: AssetType, id: str) -> str | None:
|
|
41
|
+
"""
|
|
42
|
+
Decide or determine the mime-type of an asset.
|
|
43
|
+
:param source: Where does this asset come from?
|
|
44
|
+
:param type: Type of asset
|
|
45
|
+
:param id:
|
|
46
|
+
:return: A mime type, or None if either we can't determine one or one does not apply
|
|
47
|
+
"""
|
|
48
|
+
if source is AssetSource.SMUGMUG:
|
|
49
|
+
if type is AssetType.ALBUM:
|
|
50
|
+
return None
|
|
51
|
+
if type is AssetType.IMAGE:
|
|
52
|
+
return "image/jpeg"
|
|
53
|
+
if type is AssetType.VIDEO:
|
|
54
|
+
return "video/mp4" # TODO: check if correct
|
|
55
|
+
raise ValueError(f"Unhandled asset type {type}")
|
|
56
|
+
|
|
57
|
+
if source is AssetSource.FILE:
|
|
58
|
+
guess = mimetypes.guess_type(id)[0]
|
|
59
|
+
if guess is None:
|
|
60
|
+
raise ValueError(f"Could not guess mime type for {id}")
|
|
61
|
+
return guess
|
|
62
|
+
|
|
63
|
+
raise ValueError(f"Unhandled asset source {source}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def save_asset( # noqa: PLR0913
|
|
67
|
+
target_id: str,
|
|
68
|
+
target_type: AssetTarget,
|
|
69
|
+
source: AssetSource,
|
|
70
|
+
type: AssetType,
|
|
71
|
+
id: str,
|
|
72
|
+
category: AssetCategory | str | None = None,
|
|
73
|
+
title: str | None = None,
|
|
74
|
+
page: int | None = None,
|
|
75
|
+
) -> database.Asset:
|
|
76
|
+
return database.Asset.create(
|
|
77
|
+
target_id=target_id,
|
|
78
|
+
target_type=target_type,
|
|
79
|
+
asset_source=source,
|
|
80
|
+
asset_type=type,
|
|
81
|
+
asset_mime_type=get_mime_type(source, type, id),
|
|
82
|
+
asset_id=id,
|
|
83
|
+
asset_category=category,
|
|
84
|
+
title=title,
|
|
85
|
+
page=page,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def assets_from_show_model(
|
|
90
|
+
show: models.Show,
|
|
91
|
+
) -> Generator[schema.Asset, None, None]:
|
|
92
|
+
"""Generate show assets (output type) from a show model (input type)"""
|
|
93
|
+
for asset in show.assets:
|
|
94
|
+
if asset.image:
|
|
95
|
+
source = AssetSource.SMUGMUG
|
|
96
|
+
type = AssetType.IMAGE
|
|
97
|
+
elif asset.video:
|
|
98
|
+
source = AssetSource.SMUGMUG
|
|
99
|
+
type = AssetType.VIDEO
|
|
100
|
+
elif asset.filename:
|
|
101
|
+
source = AssetSource.FILE
|
|
102
|
+
# We assume files are something else, but could be image or video.
|
|
103
|
+
# In reality they tend to be PDF / audio / other docs
|
|
104
|
+
type = AssetType.OTHER
|
|
105
|
+
else:
|
|
106
|
+
raise ValueError(f"Unhandled mode for asset {asset}")
|
|
107
|
+
|
|
108
|
+
asset_id = asset.image or asset.video or asset.filename
|
|
109
|
+
if not asset_id:
|
|
110
|
+
raise ValueError(f"No ID for asset {asset}")
|
|
111
|
+
|
|
112
|
+
yield schema.Asset(
|
|
113
|
+
type=str(type),
|
|
114
|
+
source=str(source),
|
|
115
|
+
mime_type=get_mime_type(source, type, asset_id),
|
|
116
|
+
id=asset_id,
|
|
117
|
+
category=asset.type,
|
|
118
|
+
# we use type for image/video/other, source uses it for category
|
|
119
|
+
title=asset.title,
|
|
120
|
+
page=asset.page,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if show.prod_shots:
|
|
124
|
+
yield schema.Asset(
|
|
125
|
+
type=str(AssetType.ALBUM),
|
|
126
|
+
source=str(AssetSource.SMUGMUG),
|
|
127
|
+
mime_type=get_mime_type(
|
|
128
|
+
AssetSource.SMUGMUG, AssetType.ALBUM, show.prod_shots
|
|
129
|
+
),
|
|
130
|
+
id=show.prod_shots,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def save_show_assets(
|
|
135
|
+
path: DocumentPath, show_assets: Iterable[schema.Asset]
|
|
136
|
+
) -> list[database.Asset]:
|
|
137
|
+
"""Write assets to the database"""
|
|
138
|
+
return [
|
|
139
|
+
save_asset(
|
|
140
|
+
target_id=path.id,
|
|
141
|
+
target_type=AssetTarget.SHOW,
|
|
142
|
+
source=AssetSource(asset.source),
|
|
143
|
+
type=AssetType(asset.type),
|
|
144
|
+
id=asset.id,
|
|
145
|
+
category=asset.category,
|
|
146
|
+
title=asset.title,
|
|
147
|
+
page=asset.page,
|
|
148
|
+
)
|
|
149
|
+
for asset in show_assets
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def save_person_assets(
|
|
154
|
+
path: DocumentPath, person: models.Person
|
|
155
|
+
) -> list[database.Asset]:
|
|
156
|
+
assets = []
|
|
157
|
+
if person.headshot:
|
|
158
|
+
assets.append(
|
|
159
|
+
save_asset(
|
|
160
|
+
target_id=path.id,
|
|
161
|
+
target_type=AssetTarget.PERSON,
|
|
162
|
+
source=AssetSource.SMUGMUG,
|
|
163
|
+
type=AssetType.IMAGE,
|
|
164
|
+
id=person.headshot,
|
|
165
|
+
category=AssetCategory.HEADSHOT,
|
|
166
|
+
title=person.title,
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
return assets
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def filter_assets_by_type(assets, type):
|
|
173
|
+
return list(filter(lambda asset: asset.type.lower() == type.value, assets))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def pick_show_primary_image(assets: list[models.Asset]) -> str | None:
|
|
177
|
+
"""
|
|
178
|
+
Pick an image to use as the primary, to be used in list views &c
|
|
179
|
+
TODO: Currently we return a SmugMug ID rather than a full Asset object.
|
|
180
|
+
"""
|
|
181
|
+
image_assets = list(filter(lambda asset: asset.image is not None, assets))
|
|
182
|
+
if override_assets := list(filter(lambda asset: asset.display_image, image_assets)):
|
|
183
|
+
return override_assets[0].image
|
|
184
|
+
if posters := filter_assets_by_type(image_assets, AssetCategory.POSTER):
|
|
185
|
+
return posters[0].image
|
|
186
|
+
if flyers := filter_assets_by_type(image_assets, AssetCategory.FLYER):
|
|
187
|
+
return flyers[0].image
|
|
188
|
+
if programmes := filter_assets_by_type(image_assets, AssetCategory.PROGRAMME):
|
|
189
|
+
return programmes[0].image
|
|
190
|
+
# No suitable image found, oh well we tried
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def smugmug_asset_to_asset(smugmug_asset: SmugMugImage) -> schema.Asset:
|
|
195
|
+
"""Convert a SmugMug asset to a schema asset"""
|
|
196
|
+
asset_type = AssetType.VIDEO if smugmug_asset.IsVideo else AssetType.IMAGE
|
|
197
|
+
return schema.Asset(
|
|
198
|
+
id=smugmug_asset.ImageKey,
|
|
199
|
+
source=AssetSource.SMUGMUG.value,
|
|
200
|
+
title=smugmug_asset.Title or None,
|
|
201
|
+
type=asset_type.value,
|
|
202
|
+
mime_type=get_mime_type(
|
|
203
|
+
AssetSource.SMUGMUG, asset_type, smugmug_asset.ImageKey
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def get_asset_collection_from_album(
|
|
209
|
+
album: database.Asset,
|
|
210
|
+
) -> schema.AssetCollection | None:
|
|
211
|
+
"""
|
|
212
|
+
Get AssetCollection from an asset album.
|
|
213
|
+
Currently, we only support SmugMug albums.
|
|
214
|
+
If the SmugMug data isn't present or fails to parse, we return None.
|
|
215
|
+
"""
|
|
216
|
+
if album.asset_source != AssetSource.SMUGMUG.value:
|
|
217
|
+
raise ValueError(f"Album source '{album.asset_source}' unsupported")
|
|
218
|
+
if not album.asset_smugmug_data:
|
|
219
|
+
log.warning(f"No smugmug data for album {album.asset_id}")
|
|
220
|
+
return None
|
|
221
|
+
try:
|
|
222
|
+
smugmug_album_images = json.loads(album.asset_smugmug_data)
|
|
223
|
+
smugmug_assets = [SmugMugImage(**asset) for asset in smugmug_album_images]
|
|
224
|
+
except json.JSONDecodeError:
|
|
225
|
+
log.warning(f"Could not decode smugmug data for album {album.asset_id}")
|
|
226
|
+
return None
|
|
227
|
+
return schema.AssetCollection(
|
|
228
|
+
[smugmug_asset_to_asset(asset) for asset in smugmug_assets]
|
|
229
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from pydantic_settings import BaseSettings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Settings(BaseSettings):
|
|
8
|
+
db_uri: str = "nthp.db"
|
|
9
|
+
branch: str = "master"
|
|
10
|
+
content_root: Path
|
|
11
|
+
|
|
12
|
+
year_start: int = 1940
|
|
13
|
+
year_end: int = datetime.datetime.now().year
|
|
14
|
+
|
|
15
|
+
# How many years to wait until guessing someone has left, if it's not been this
|
|
16
|
+
# long we can assume they may still be a student.
|
|
17
|
+
graduation_recency_limit: int = 2
|
|
18
|
+
# What month (1-12) do people tend to graduate in?
|
|
19
|
+
graduation_month: int = 6
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
settings = Settings()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from io import StringIO
|
|
2
|
+
|
|
3
|
+
import markdown
|
|
4
|
+
from markdown import Markdown
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def unmark_element(element, stream=None):
|
|
8
|
+
if stream is None:
|
|
9
|
+
stream = StringIO()
|
|
10
|
+
if element.text:
|
|
11
|
+
stream.write(element.text)
|
|
12
|
+
for sub in element:
|
|
13
|
+
unmark_element(sub, stream)
|
|
14
|
+
if element.tail:
|
|
15
|
+
stream.write(element.tail)
|
|
16
|
+
return stream.getvalue()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Make a markdown parser that outputs plaintext
|
|
20
|
+
# Stole from https://stackoverflow.com/a/54923798/1345360
|
|
21
|
+
Markdown.output_formats["plain"] = unmark_element # type: ignore
|
|
22
|
+
_markdown_unmarker = Markdown(output_format="plain") # type: ignore
|
|
23
|
+
_markdown_unmarker.stripTopLevelTags = False # type: ignore
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def markdown_to_html(markdown_text: str | None) -> str | None:
|
|
27
|
+
if not markdown_text:
|
|
28
|
+
return None
|
|
29
|
+
if not markdown_text.strip():
|
|
30
|
+
return None
|
|
31
|
+
return markdown.markdown(markdown_text)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def markdown_to_plaintext(markdown_text: str | None) -> str | None:
|
|
35
|
+
if not markdown_text:
|
|
36
|
+
return None
|
|
37
|
+
if not markdown_text.strip():
|
|
38
|
+
return None
|
|
39
|
+
return _markdown_unmarker.convert(markdown_text)
|