nthp_api 0.1.9__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.
- nthp_api/__init__.py +0 -0
- nthp_api/cli/__init__.py +88 -0
- nthp_api/cli/logs.py +20 -0
- nthp_api/nthp_build/__init__.py +0 -0
- nthp_api/nthp_build/assets.py +229 -0
- nthp_api/nthp_build/config.py +22 -0
- nthp_api/nthp_build/content.py +39 -0
- nthp_api/nthp_build/database.py +142 -0
- nthp_api/nthp_build/documents.py +55 -0
- nthp_api/nthp_build/dumper.py +385 -0
- nthp_api/nthp_build/fields.py +15 -0
- nthp_api/nthp_build/history.py +17 -0
- nthp_api/nthp_build/loader.py +280 -0
- nthp_api/nthp_build/models.py +200 -0
- nthp_api/nthp_build/parallel.py +57 -0
- nthp_api/nthp_build/people.py +227 -0
- nthp_api/nthp_build/playwrights.py +137 -0
- nthp_api/nthp_build/roles.py +213 -0
- nthp_api/nthp_build/schema.py +440 -0
- nthp_api/nthp_build/search.py +17 -0
- nthp_api/nthp_build/shows.py +182 -0
- nthp_api/nthp_build/smugmug.py +40 -0
- nthp_api/nthp_build/spec.py +298 -0
- nthp_api/nthp_build/trivia.py +65 -0
- nthp_api/nthp_build/venues.py +69 -0
- nthp_api/nthp_build/version.py +8 -0
- nthp_api/nthp_build/years.py +40 -0
- nthp_api/smugmugger/__init__.py +12 -0
- nthp_api/smugmugger/album.py +20 -0
- nthp_api/smugmugger/client.py +118 -0
- nthp_api/smugmugger/config.py +16 -0
- nthp_api/smugmugger/database.py +33 -0
- nthp_api/smugmugger/schema.py +58 -0
- nthp_api/smugmugger/smugmug.py +67 -0
- nthp_api-0.1.9.dist-info/METADATA +82 -0
- nthp_api-0.1.9.dist-info/RECORD +39 -0
- nthp_api-0.1.9.dist-info/WHEEL +4 -0
- nthp_api-0.1.9.dist-info/entry_points.txt +4 -0
- nthp_api-0.1.9.dist-info/licenses/LICENCE +20 -0
nthp_api/__init__.py
ADDED
|
File without changes
|
nthp_api/cli/__init__.py
ADDED
|
@@ -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()
|
nthp_api/cli/logs.py
ADDED
|
@@ -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)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import peewee
|
|
4
|
+
|
|
5
|
+
from nthp_api.nthp_build.config import settings
|
|
6
|
+
|
|
7
|
+
log = logging.getLogger(__name__)
|
|
8
|
+
db = peewee.SqliteDatabase(settings.db_uri)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NthpDbModel(peewee.Model):
|
|
12
|
+
class Meta:
|
|
13
|
+
database = db
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DbCompatEnumMixin:
|
|
17
|
+
def __str__(self):
|
|
18
|
+
return self.value
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TargetType:
|
|
22
|
+
SHOW = "show"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PersonRoleType:
|
|
26
|
+
CAST = "CAST"
|
|
27
|
+
CREW = "CREW"
|
|
28
|
+
COMMITTEE = "COMMITTEE"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PersonRole(NthpDbModel):
|
|
32
|
+
target_id = peewee.CharField(index=True)
|
|
33
|
+
target_type = peewee.CharField(index=True)
|
|
34
|
+
# Uses YYYY, not YY_YY, 2000 means 2000-2001
|
|
35
|
+
target_year = peewee.IntegerField(index=True)
|
|
36
|
+
|
|
37
|
+
person_id = peewee.CharField(index=True, null=True)
|
|
38
|
+
person_name = peewee.CharField(null=True)
|
|
39
|
+
role = peewee.CharField(null=True, index=True)
|
|
40
|
+
is_person = peewee.BooleanField(index=True)
|
|
41
|
+
data = peewee.TextField()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Show(NthpDbModel):
|
|
45
|
+
id = peewee.CharField(primary_key=True)
|
|
46
|
+
source_path = peewee.CharField()
|
|
47
|
+
year = peewee.IntegerField()
|
|
48
|
+
year_id = peewee.CharField(index=True)
|
|
49
|
+
title = peewee.CharField()
|
|
50
|
+
venue_id = peewee.CharField(index=True, null=True)
|
|
51
|
+
season_sort = peewee.IntegerField(null=True, index=True)
|
|
52
|
+
date_start = peewee.DateField(null=True, index=True)
|
|
53
|
+
date_end = peewee.DateField(null=True)
|
|
54
|
+
primary_image = peewee.CharField(null=True)
|
|
55
|
+
assets = peewee.TextField()
|
|
56
|
+
data = peewee.TextField()
|
|
57
|
+
content = peewee.TextField(null=True)
|
|
58
|
+
plaintext = peewee.TextField(null=True)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PlaywrightShow(NthpDbModel):
|
|
62
|
+
play_id = peewee.CharField(index=True)
|
|
63
|
+
play_name = peewee.CharField()
|
|
64
|
+
playwright_id = peewee.CharField(index=True)
|
|
65
|
+
playwright_name = peewee.CharField()
|
|
66
|
+
show_id = peewee.CharField(index=True)
|
|
67
|
+
person_id = peewee.CharField(null=True, index=True)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Venue(NthpDbModel):
|
|
71
|
+
id = peewee.CharField(primary_key=True)
|
|
72
|
+
name = peewee.CharField()
|
|
73
|
+
data = peewee.TextField()
|
|
74
|
+
content = peewee.TextField(null=True)
|
|
75
|
+
plaintext = peewee.TextField(null=True)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Person(NthpDbModel):
|
|
79
|
+
id = peewee.CharField(primary_key=True)
|
|
80
|
+
title = peewee.CharField()
|
|
81
|
+
graduated = peewee.IntegerField(index=True, null=True)
|
|
82
|
+
headshot = peewee.CharField(null=True)
|
|
83
|
+
data = peewee.TextField()
|
|
84
|
+
content = peewee.TextField(null=True)
|
|
85
|
+
plaintext = peewee.TextField(null=True)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Trivia(NthpDbModel):
|
|
89
|
+
target_id = peewee.CharField(index=True)
|
|
90
|
+
target_type = peewee.CharField(index=True)
|
|
91
|
+
target_name = peewee.CharField()
|
|
92
|
+
target_image_id = peewee.CharField(null=True)
|
|
93
|
+
# Uses YYYY, not YY_YY, 2000 means 2000-2001
|
|
94
|
+
target_year = peewee.IntegerField(index=True, null=True)
|
|
95
|
+
|
|
96
|
+
person_id = peewee.CharField(index=True, null=True)
|
|
97
|
+
person_name = peewee.CharField(null=True)
|
|
98
|
+
|
|
99
|
+
quote = peewee.TextField()
|
|
100
|
+
submitted = peewee.DateField(null=True, index=True)
|
|
101
|
+
|
|
102
|
+
data = peewee.TextField()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class HistoryRecord(NthpDbModel):
|
|
106
|
+
year = peewee.CharField()
|
|
107
|
+
academic_year = peewee.CharField(null=True, index=True)
|
|
108
|
+
title = peewee.CharField()
|
|
109
|
+
description = peewee.TextField()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class Asset(NthpDbModel):
|
|
113
|
+
target_id = peewee.CharField(index=True)
|
|
114
|
+
target_type = peewee.CharField(index=True)
|
|
115
|
+
|
|
116
|
+
asset_source = peewee.CharField(index=True)
|
|
117
|
+
asset_type = peewee.CharField(index=True)
|
|
118
|
+
asset_mime_type = peewee.CharField(null=True)
|
|
119
|
+
asset_id = peewee.CharField(index=True)
|
|
120
|
+
|
|
121
|
+
asset_category = peewee.CharField(null=True)
|
|
122
|
+
asset_title = peewee.CharField(null=True)
|
|
123
|
+
asset_page = peewee.IntegerField(null=True)
|
|
124
|
+
|
|
125
|
+
asset_smugmug_data = peewee.CharField(null=True)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
MODELS = [Show, PlaywrightShow, Venue, PersonRole, Person, Trivia, HistoryRecord, Asset]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def init_db(create: bool = False):
|
|
132
|
+
log.info(f"Initializing database: {db.database}")
|
|
133
|
+
|
|
134
|
+
db.connect()
|
|
135
|
+
if create:
|
|
136
|
+
db.drop_tables(MODELS)
|
|
137
|
+
db.create_tables(MODELS)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def show_stats():
|
|
141
|
+
for model in MODELS:
|
|
142
|
+
log.info(f"{model.__name__} has {model.select().count()} records")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Iterable
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, NamedTuple
|
|
5
|
+
|
|
6
|
+
import frontmatter
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from nthp_api.nthp_build.config import settings
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DocumentPath(NamedTuple):
|
|
15
|
+
path: Path
|
|
16
|
+
id: str
|
|
17
|
+
content_path: Path
|
|
18
|
+
filename: str
|
|
19
|
+
basename: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def find_documents(content_directory: Path | str) -> Iterable[DocumentPath]:
|
|
23
|
+
def map_path(path: Path) -> DocumentPath | None:
|
|
24
|
+
if path.name.startswith("_"):
|
|
25
|
+
return None
|
|
26
|
+
return DocumentPath(
|
|
27
|
+
path=path,
|
|
28
|
+
id=str(
|
|
29
|
+
path.relative_to(settings.content_root / content_directory).parent
|
|
30
|
+
/ path.stem
|
|
31
|
+
).lstrip("_"),
|
|
32
|
+
content_path=path.relative_to(settings.content_root),
|
|
33
|
+
filename=path.name,
|
|
34
|
+
basename=path.stem,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return [
|
|
38
|
+
doc_path
|
|
39
|
+
for doc_path in map(
|
|
40
|
+
map_path, (settings.content_root / Path(content_directory)).rglob("*.md")
|
|
41
|
+
)
|
|
42
|
+
if doc_path is not None
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load_document(path: Path) -> frontmatter.Post:
|
|
47
|
+
return frontmatter.load(path)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_yaml(path: Path | str) -> Any:
|
|
51
|
+
with (settings.content_root / Path(path)).open() as stream:
|
|
52
|
+
try:
|
|
53
|
+
return yaml.safe_load(stream)
|
|
54
|
+
except yaml.YAMLError:
|
|
55
|
+
log.exception("Error loading YAML file %s", path)
|