structables 0.4.4__tar.gz → 0.4.6__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.
- {structables-0.4.4 → structables-0.4.6}/.dockerignore +2 -0
- structables-0.4.6/.forgejo/workflows/docker.yml +59 -0
- {structables-0.4.4 → structables-0.4.6}/Dockerfile +4 -1
- {structables-0.4.4 → structables-0.4.6}/PKG-INFO +4 -3
- {structables-0.4.4 → structables-0.4.6}/README.md +0 -1
- {structables-0.4.4 → structables-0.4.6}/instances.json +0 -10
- {structables-0.4.4 → structables-0.4.6}/pyproject.toml +6 -1
- structables-0.4.6/scripts/create_release.py +153 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/config.py +11 -10
- {structables-0.4.4 → structables-0.4.6}/src/structables/main.py +1 -3
- {structables-0.4.4 → structables-0.4.6}/src/structables/routes/__init__.py +4 -3
- {structables-0.4.4 → structables-0.4.6}/src/structables/routes/category.py +8 -2
- {structables-0.4.4 → structables-0.4.6}/src/structables/routes/contest.py +29 -43
- {structables-0.4.4 → structables-0.4.6}/src/structables/routes/main.py +85 -95
- structables-0.4.6/src/structables/routes/member.py +97 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/routes/proxy.py +54 -38
- {structables-0.4.4 → structables-0.4.6}/src/structables/static/css/style.css +163 -47
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/archives.html +1 -1
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/article.html +12 -8
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/category.html +2 -2
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/collection.html +2 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/contest.html +3 -3
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/contests.html +1 -1
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/index.html +1 -1
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/member-instructables.html +13 -4
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/member.html +8 -6
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/projects.html +1 -1
- structables-0.4.6/src/structables/templates/sitemap.html +19 -0
- structables-0.4.6/src/structables/utils/cache.py +23 -0
- structables-0.4.6/src/structables/utils/data.py +89 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/utils/helpers.py +132 -146
- structables-0.4.4/.forgejo/workflows/docker.yml +0 -37
- structables-0.4.4/src/structables/routes/member.py +0 -166
- structables-0.4.4/src/structables/templates/sitemap.html +0 -27
- structables-0.4.4/src/structables/utils/data.py +0 -99
- {structables-0.4.4 → structables-0.4.6}/.env.example +0 -0
- {structables-0.4.4 → structables-0.4.6}/.forgejo/workflows/release.yml +0 -0
- {structables-0.4.4 → structables-0.4.6}/.gitignore +0 -0
- {structables-0.4.4 → structables-0.4.6}/LICENSE +0 -0
- {structables-0.4.4 → structables-0.4.6}/cronjob.txt +0 -0
- {structables-0.4.4 → structables-0.4.6}/docker-compose-example-dev.yml +0 -0
- {structables-0.4.4 → structables-0.4.6}/docker-compose-example.yml +0 -0
- {structables-0.4.4 → structables-0.4.6}/entrypoint.sh +0 -0
- {structables-0.4.4 → structables-0.4.6}/renovate.json +0 -0
- {structables-0.4.4 → structables-0.4.6}/requirements-dev.txt +0 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/__init__.py +0 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/static/css/iframe.css +0 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/static/img/logo.png +0 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/static/img/logo_lg.png +0 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/static/img/magnifying-glass-solid.svg +0 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/400.html +0 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/404.html +0 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/429.html +0 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/500.html +0 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/article-review.html +0 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/base.html +0 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/footer.html +0 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/header.html +0 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/iframe.html +0 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/privacypolicy.html +0 -0
- {structables-0.4.4 → structables-0.4.6}/src/structables/templates/style.html +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: Docker CI/CD
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
enable-openid-connect: true
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
docker:
|
|
13
|
+
name: Docker Build and Push to git.private.coffee
|
|
14
|
+
runs-on: docker-privileged
|
|
15
|
+
container:
|
|
16
|
+
image: node:22-bookworm
|
|
17
|
+
options: >-
|
|
18
|
+
--privileged
|
|
19
|
+
steps:
|
|
20
|
+
- name: Check out code
|
|
21
|
+
uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: |
|
|
25
|
+
apt update
|
|
26
|
+
apt install -y docker.io jq
|
|
27
|
+
|
|
28
|
+
- name: Set up QEMU
|
|
29
|
+
uses: docker/setup-qemu-action@v3
|
|
30
|
+
|
|
31
|
+
- name: Set up Docker Buildx
|
|
32
|
+
uses: docker/setup-buildx-action@v3
|
|
33
|
+
|
|
34
|
+
- name: Fetch JWT
|
|
35
|
+
id: jwt
|
|
36
|
+
env:
|
|
37
|
+
AUD: u:5:f67ef752-45f6-4a9d-933c-986806a9dc84
|
|
38
|
+
run: |
|
|
39
|
+
jwt=$(
|
|
40
|
+
curl --fail \
|
|
41
|
+
-H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
|
|
42
|
+
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=$AUD" \
|
|
43
|
+
| jq -r ".value"
|
|
44
|
+
)
|
|
45
|
+
echo "::add-mask::$jwt"
|
|
46
|
+
echo "jwt=$jwt" >> $FORGEJO_OUTPUT
|
|
47
|
+
|
|
48
|
+
- name: Login to Forgejo
|
|
49
|
+
run: |
|
|
50
|
+
echo "${{ steps.jwt.outputs.jwt }}" \
|
|
51
|
+
| docker login --username token --password-stdin git.private.coffee
|
|
52
|
+
|
|
53
|
+
- name: Build and push to Forgejo
|
|
54
|
+
uses: docker/build-push-action@v6
|
|
55
|
+
with:
|
|
56
|
+
push: true
|
|
57
|
+
tags: |
|
|
58
|
+
git.private.coffee/privatecoffee/structables:latest
|
|
59
|
+
git.private.coffee/privatecoffee/structables:${{ env.GITHUB_REF_NAME }}
|
|
@@ -11,10 +11,13 @@ COPY . /app
|
|
|
11
11
|
|
|
12
12
|
RUN $APP_ENV/bin/pip install --no-cache-dir pip && \
|
|
13
13
|
$APP_ENV/bin/pip install /app && \
|
|
14
|
-
adduser -S -D -H structables
|
|
14
|
+
adduser -S -D -H structables && \
|
|
15
|
+
mkdir -p /tmp/structables_cache && chown structables /tmp/structables_cache
|
|
15
16
|
|
|
16
17
|
EXPOSE 8002
|
|
17
18
|
|
|
18
19
|
USER structables
|
|
19
20
|
|
|
21
|
+
STOPSIGNAL SIGQUIT
|
|
22
|
+
|
|
20
23
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: structables
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.6
|
|
4
4
|
Summary: A simple frontend for Instructables
|
|
5
5
|
Project-URL: Homepage, https://git.private.coffee/privatecoffee/structables
|
|
6
6
|
Project-URL: Bug Tracker, https://git.private.coffee/privatecoffee/structables/issues
|
|
@@ -675,6 +675,8 @@ Requires-Python: >=3.10
|
|
|
675
675
|
Requires-Dist: beautifulsoup4
|
|
676
676
|
Requires-Dist: flask
|
|
677
677
|
Requires-Dist: markdown2[all]
|
|
678
|
+
Provides-Extra: dev
|
|
679
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
678
680
|
Description-Content-Type: text/markdown
|
|
679
681
|
|
|
680
682
|
<img align="right" src="src/structables/static/img/logo.png">
|
|
@@ -699,7 +701,6 @@ An open source alternative front-end to Instructables. This is a fork of <a href
|
|
|
699
701
|
| [structables.private.coffee](https://structables.private.coffee) | [Private.coffee](https://private.coffee) | Austria 🇦🇹 🇪🇺 | Main instance |
|
|
700
702
|
| [structables.bloat.cat](https://structables.bloat.cat) | [Bloat.cat](https://bloat.cat) | Germany 🇩🇪 🇪🇺 | |
|
|
701
703
|
| [structables.darkness.services](https://structables.darkness.services) | [Darkness.services](https://darkness.services) | United States 🇺🇸 | |
|
|
702
|
-
| [structables.franklyflawless.org](https://structables.franklyflawless.org) | [FranklyFlawless](https://franklyflawless.org) | Germany 🇩🇪 🇪🇺 | |
|
|
703
704
|
|
|
704
705
|
<!-- END_INSTANCE_LIST -->
|
|
705
706
|
|
|
@@ -20,7 +20,6 @@ An open source alternative front-end to Instructables. This is a fork of <a href
|
|
|
20
20
|
| [structables.private.coffee](https://structables.private.coffee) | [Private.coffee](https://private.coffee) | Austria 🇦🇹 🇪🇺 | Main instance |
|
|
21
21
|
| [structables.bloat.cat](https://structables.bloat.cat) | [Bloat.cat](https://bloat.cat) | Germany 🇩🇪 🇪🇺 | |
|
|
22
22
|
| [structables.darkness.services](https://structables.darkness.services) | [Darkness.services](https://darkness.services) | United States 🇺🇸 | |
|
|
23
|
-
| [structables.franklyflawless.org](https://structables.franklyflawless.org) | [FranklyFlawless](https://franklyflawless.org) | Germany 🇩🇪 🇪🇺 | |
|
|
24
23
|
|
|
25
24
|
<!-- END_INSTANCE_LIST -->
|
|
26
25
|
|
|
@@ -50,15 +50,5 @@
|
|
|
50
50
|
"url": "https://darkness.services"
|
|
51
51
|
},
|
|
52
52
|
"location": "US"
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"name": "structables.franklyflawless.org",
|
|
56
|
-
"url": "https://structables.franklyflawless.org",
|
|
57
|
-
"type": "clearnet",
|
|
58
|
-
"provider": {
|
|
59
|
-
"name": "FranklyFlawless",
|
|
60
|
-
"url": "https://franklyflawless.org"
|
|
61
|
-
},
|
|
62
|
-
"location": "DE"
|
|
63
53
|
}
|
|
64
54
|
]
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "structables"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.6"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Private.coffee Team", email="support@private.coffee" },
|
|
10
10
|
]
|
|
@@ -23,6 +23,11 @@ dependencies = [
|
|
|
23
23
|
"markdown2[all]",
|
|
24
24
|
]
|
|
25
25
|
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = [
|
|
28
|
+
"ruff",
|
|
29
|
+
]
|
|
30
|
+
|
|
26
31
|
[project.scripts]
|
|
27
32
|
structables = "structables.main:main"
|
|
28
33
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
VERSION_PATTERN = re.compile(
|
|
10
|
+
r'(?m)^(version\s*=\s*")(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(")$'
|
|
11
|
+
)
|
|
12
|
+
PRE_RELEASE_COMMANDS = (
|
|
13
|
+
("-m", "ruff", "check", "src", "scripts"),
|
|
14
|
+
("-m", "ruff", "format", "--check", "src", "scripts"),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse_args() -> argparse.Namespace:
|
|
19
|
+
parser = argparse.ArgumentParser(
|
|
20
|
+
description=(
|
|
21
|
+
"Bump the project version in pyproject.toml and create a release commit "
|
|
22
|
+
"and tag."
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
group = parser.add_mutually_exclusive_group(required=True)
|
|
26
|
+
group.add_argument(
|
|
27
|
+
"--major",
|
|
28
|
+
action="store_true",
|
|
29
|
+
help="Bump the major version and reset minor/patch.",
|
|
30
|
+
)
|
|
31
|
+
group.add_argument(
|
|
32
|
+
"--minor", action="store_true", help="Bump the minor version and reset patch."
|
|
33
|
+
)
|
|
34
|
+
group.add_argument("--patch", action="store_true", help="Bump the patch version.")
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--skip-checks",
|
|
37
|
+
"--skip-lint",
|
|
38
|
+
dest="skip_checks",
|
|
39
|
+
action="store_true",
|
|
40
|
+
help="Skip the pre-release checks.",
|
|
41
|
+
)
|
|
42
|
+
return parser.parse_args()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run_git(*args: str, repo_root: Path) -> str:
|
|
46
|
+
result = subprocess.run(
|
|
47
|
+
["git", *args],
|
|
48
|
+
cwd=repo_root,
|
|
49
|
+
check=True,
|
|
50
|
+
text=True,
|
|
51
|
+
capture_output=True,
|
|
52
|
+
)
|
|
53
|
+
return result.stdout.strip()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def run_python(*args: str, repo_root: Path) -> None:
|
|
57
|
+
subprocess.run(
|
|
58
|
+
[sys.executable, *args],
|
|
59
|
+
cwd=repo_root,
|
|
60
|
+
check=True,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def ensure_clean_worktree(repo_root: Path) -> None:
|
|
65
|
+
status = run_git("status", "--short", repo_root=repo_root)
|
|
66
|
+
if status:
|
|
67
|
+
raise SystemExit("Working tree is not clean. Commit or stash changes first.")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def load_version(pyproject_path: Path) -> tuple[str, tuple[int, int, int]]:
|
|
71
|
+
content = pyproject_path.read_text(encoding="utf-8")
|
|
72
|
+
match = VERSION_PATTERN.search(content)
|
|
73
|
+
if not match:
|
|
74
|
+
raise SystemExit("Could not find [project] version in pyproject.toml.")
|
|
75
|
+
version = (
|
|
76
|
+
int(match.group("major")),
|
|
77
|
+
int(match.group("minor")),
|
|
78
|
+
int(match.group("patch")),
|
|
79
|
+
)
|
|
80
|
+
return content, version
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def bump_version(
|
|
84
|
+
version: tuple[int, int, int], args: argparse.Namespace
|
|
85
|
+
) -> tuple[int, int, int]:
|
|
86
|
+
major, minor, patch = version
|
|
87
|
+
if args.major:
|
|
88
|
+
return major + 1, 0, 0
|
|
89
|
+
if args.minor:
|
|
90
|
+
return major, minor + 1, 0
|
|
91
|
+
return major, minor, patch + 1
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def write_version(
|
|
95
|
+
pyproject_path: Path, content: str, new_version: tuple[int, int, int]
|
|
96
|
+
) -> str:
|
|
97
|
+
version_text = ".".join(str(part) for part in new_version)
|
|
98
|
+
updated = VERSION_PATTERN.sub(rf"\g<1>{version_text}\g<5>", content, count=1)
|
|
99
|
+
pyproject_path.write_text(updated, encoding="utf-8")
|
|
100
|
+
return version_text
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def ensure_tag_does_not_exist(repo_root: Path, tag_name: str) -> None:
|
|
104
|
+
if run_git("tag", "--list", tag_name, repo_root=repo_root):
|
|
105
|
+
raise SystemExit(f"Tag {tag_name} already exists.")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def ensure_pre_release_checks_pass(repo_root: Path) -> None:
|
|
109
|
+
for command in PRE_RELEASE_COMMANDS:
|
|
110
|
+
try:
|
|
111
|
+
run_python(*command, repo_root=repo_root)
|
|
112
|
+
except subprocess.CalledProcessError as exc:
|
|
113
|
+
raise SystemExit(
|
|
114
|
+
"Pre-release checks failed. Fix the errors or re-run with --skip-checks."
|
|
115
|
+
) from exc
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def create_release_commit(repo_root: Path, tag_name: str) -> None:
|
|
119
|
+
run_git("add", "pyproject.toml", repo_root=repo_root)
|
|
120
|
+
run_git("commit", "-m", f"Release {tag_name}", repo_root=repo_root)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def create_tag(repo_root: Path, tag_name: str) -> None:
|
|
124
|
+
run_git("tag", "-m", f"Release {tag_name}", tag_name, repo_root=repo_root)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main() -> int:
|
|
128
|
+
args = parse_args()
|
|
129
|
+
repo_root = Path(__file__).resolve().parent.parent
|
|
130
|
+
pyproject_path = repo_root / "pyproject.toml"
|
|
131
|
+
|
|
132
|
+
ensure_clean_worktree(repo_root)
|
|
133
|
+
if not args.skip_checks:
|
|
134
|
+
ensure_pre_release_checks_pass(repo_root)
|
|
135
|
+
content, current_version = load_version(pyproject_path)
|
|
136
|
+
new_version = bump_version(current_version, args)
|
|
137
|
+
version_text = ".".join(str(part) for part in new_version)
|
|
138
|
+
tag_name = f"v{version_text}"
|
|
139
|
+
ensure_tag_does_not_exist(repo_root, tag_name)
|
|
140
|
+
write_version(pyproject_path, content, new_version)
|
|
141
|
+
create_release_commit(repo_root, tag_name)
|
|
142
|
+
create_tag(repo_root, tag_name)
|
|
143
|
+
print(f"Released {tag_name}")
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
try:
|
|
149
|
+
raise SystemExit(main())
|
|
150
|
+
except subprocess.CalledProcessError as exc:
|
|
151
|
+
if exc.stderr:
|
|
152
|
+
print(exc.stderr.strip(), file=sys.stderr)
|
|
153
|
+
raise SystemExit(exc.returncode) from exc
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import os
|
|
2
3
|
import tempfile
|
|
3
|
-
import logging
|
|
4
|
-
|
|
5
4
|
from pathlib import Path
|
|
6
5
|
|
|
7
6
|
from .utils.helpers import get_typesense_api_key
|
|
@@ -10,11 +9,11 @@ logger = logging.getLogger(__name__)
|
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
class Config:
|
|
13
|
-
DEBUG = os.environ.get("FLASK_DEBUG", os.environ.get("STRUCTABLES_DEBUG"
|
|
14
|
-
PORT = int(os.environ.get("STRUCTABLES_PORT", 8002))
|
|
12
|
+
DEBUG = os.environ.get("FLASK_DEBUG", os.environ.get("STRUCTABLES_DEBUG"))
|
|
13
|
+
PORT = int(os.environ.get("STRUCTABLES_PORT", "8002"))
|
|
15
14
|
LISTEN_HOST = os.environ.get("STRUCTABLES_LISTEN_HOST", "127.0.0.1")
|
|
16
15
|
INVIDIOUS = os.environ.get("STRUCTABLES_INVIDIOUS")
|
|
17
|
-
UNSAFE = os.environ.get("STRUCTABLES_UNSAFE"
|
|
16
|
+
UNSAFE = os.environ.get("STRUCTABLES_UNSAFE")
|
|
18
17
|
PRIVACY_FILE = os.environ.get("STRUCTABLES_PRIVACY_FILE")
|
|
19
18
|
THEME = os.environ.get("STRUCTABLES_THEME", "auto")
|
|
20
19
|
TYPESENSE_API_KEY = get_typesense_api_key()
|
|
@@ -39,19 +38,21 @@ class Config:
|
|
|
39
38
|
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
40
39
|
(CACHE_DIR / "test").write_text("test")
|
|
41
40
|
(CACHE_DIR / "test").unlink()
|
|
42
|
-
except Exception as e:
|
|
43
|
-
logger.error(
|
|
41
|
+
except Exception as e: # noqa: BLE001
|
|
42
|
+
logger.error(
|
|
43
|
+
f"Could not create or write to cache directory: {e} - disabling cache"
|
|
44
|
+
)
|
|
44
45
|
CACHE_ENABLED = False
|
|
45
46
|
CACHE_DIR = None
|
|
46
47
|
|
|
47
48
|
CACHE_MAX_AGE = int(
|
|
48
|
-
os.environ.get("STRUCTABLES_CACHE_MAX_AGE",
|
|
49
|
+
os.environ.get("STRUCTABLES_CACHE_MAX_AGE", "604800")
|
|
49
50
|
) # 1 week default
|
|
50
51
|
CACHE_MAX_SIZE = int(
|
|
51
|
-
os.environ.get("STRUCTABLES_CACHE_MAX_SIZE",
|
|
52
|
+
os.environ.get("STRUCTABLES_CACHE_MAX_SIZE", "1073741824")
|
|
52
53
|
) # 1GB default
|
|
53
54
|
CACHE_CLEANUP_INTERVAL = int(
|
|
54
|
-
os.environ.get("STRUCTABLES_CACHE_CLEANUP_INTERVAL",
|
|
55
|
+
os.environ.get("STRUCTABLES_CACHE_CLEANUP_INTERVAL", "3600")
|
|
55
56
|
) # 1 hour default
|
|
56
57
|
|
|
57
58
|
@staticmethod
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
from .main import init_main_routes
|
|
2
1
|
from .category import init_category_routes
|
|
2
|
+
from .contest import init_contest_routes
|
|
3
|
+
from .main import init_main_routes
|
|
3
4
|
from .member import init_member_routes
|
|
4
5
|
from .proxy import init_proxy_routes
|
|
5
|
-
|
|
6
|
+
|
|
6
7
|
|
|
7
8
|
def init_routes(app):
|
|
8
9
|
init_main_routes(app)
|
|
9
10
|
init_category_routes(app)
|
|
10
11
|
init_member_routes(app)
|
|
11
12
|
init_proxy_routes(app)
|
|
12
|
-
init_contest_routes(app)
|
|
13
|
+
init_contest_routes(app)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
1
3
|
from flask import redirect
|
|
2
4
|
from werkzeug.exceptions import NotFound
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
+
|
|
6
|
+
from ..utils.helpers import category_page, project_list
|
|
5
7
|
|
|
6
8
|
logger = logging.getLogger(__name__)
|
|
7
9
|
|
|
@@ -58,6 +60,10 @@ def init_category_routes(app):
|
|
|
58
60
|
logger.debug("Rendering craft category page")
|
|
59
61
|
return category_page(app, "Craft")
|
|
60
62
|
|
|
63
|
+
@app.route("/design/")
|
|
64
|
+
def route_design():
|
|
65
|
+
return category_page(app, "Design")
|
|
66
|
+
|
|
61
67
|
@app.route("/cooking/")
|
|
62
68
|
def route_cooking():
|
|
63
69
|
logger.debug("Rendering cooking category page")
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
from flask import render_template, request, abort, url_for
|
|
2
|
-
from urllib.request import urlopen, Request
|
|
3
|
-
from urllib.error import HTTPError
|
|
4
|
-
from ..utils.helpers import proxy
|
|
5
|
-
from bs4 import BeautifulSoup
|
|
6
|
-
import json
|
|
7
1
|
import logging
|
|
2
|
+
from urllib.error import HTTPError
|
|
3
|
+
from urllib.parse import urlencode
|
|
4
|
+
|
|
5
|
+
from flask import abort, render_template, request, url_for
|
|
6
|
+
|
|
7
|
+
from ..utils.cache import get_cached_json
|
|
8
|
+
from ..utils.helpers import proxy, thumbnail
|
|
8
9
|
|
|
9
10
|
logger = logging.getLogger(__name__)
|
|
10
11
|
|
|
@@ -20,19 +21,17 @@ def init_contest_routes(app):
|
|
|
20
21
|
logger.debug(f"Fetching contest archive page {page} with limit {limit}")
|
|
21
22
|
|
|
22
23
|
try:
|
|
23
|
-
# Fetch data using urlopen
|
|
24
24
|
url = f"https://www.instructables.com/json-api/getClosedContests?limit={limit}&offset={offset}"
|
|
25
25
|
logger.debug(f"Making request to {url}")
|
|
26
|
-
|
|
27
|
-
data = json.loads(response.read().decode())
|
|
26
|
+
data = get_cached_json(app, url)
|
|
28
27
|
logger.debug(
|
|
29
28
|
f"Received contest archive data with {len(data.get('contests', []))} contests"
|
|
30
29
|
)
|
|
31
30
|
except HTTPError as e:
|
|
32
31
|
logger.error(f"HTTP error fetching contest archive: {e.code}")
|
|
33
32
|
abort(e.code)
|
|
34
|
-
except Exception as e:
|
|
35
|
-
logger.error(f"Error fetching contest archive: {
|
|
33
|
+
except Exception as e: # noqa: BLE001
|
|
34
|
+
logger.error(f"Error fetching contest archive: {e!s}")
|
|
36
35
|
abort(500) # Handle other exceptions like JSON decode errors
|
|
37
36
|
|
|
38
37
|
contests = data.get("contests", [])
|
|
@@ -85,9 +84,7 @@ def init_contest_routes(app):
|
|
|
85
84
|
try:
|
|
86
85
|
url = f"{base_url}?q=*&filter_by=contestPath:{contest}&sort_by=contestEntryDate:desc&per_page={per_page}&page={page}"
|
|
87
86
|
logger.debug(f"Making request to {url} (page {page})")
|
|
88
|
-
|
|
89
|
-
response = urlopen(request)
|
|
90
|
-
data = json.loads(response.read().decode())
|
|
87
|
+
data = get_cached_json(app, url, headers=headers)
|
|
91
88
|
except HTTPError as e:
|
|
92
89
|
logger.error(f"HTTP error fetching contest entries: {e.code}")
|
|
93
90
|
abort(e.code)
|
|
@@ -99,7 +96,7 @@ def init_contest_routes(app):
|
|
|
99
96
|
break
|
|
100
97
|
|
|
101
98
|
all_entries.extend(hits)
|
|
102
|
-
if len(
|
|
99
|
+
if len(all_entries) >= data["found"]:
|
|
103
100
|
break
|
|
104
101
|
page += 1
|
|
105
102
|
|
|
@@ -111,32 +108,21 @@ def init_contest_routes(app):
|
|
|
111
108
|
logger.debug(f"Fetching contest page for: {contest}")
|
|
112
109
|
|
|
113
110
|
try:
|
|
114
|
-
data =
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
title =
|
|
111
|
+
data = get_cached_json(
|
|
112
|
+
app,
|
|
113
|
+
"https://www.instructables.com/json-api/contest?"
|
|
114
|
+
+ urlencode({"path": contest}),
|
|
115
|
+
)
|
|
116
|
+
title = data["title"]
|
|
120
117
|
logger.debug(f"Contest title: {title}")
|
|
121
118
|
|
|
122
|
-
|
|
123
|
-
img = img_tag.get("src") if img_tag else "default.jpg"
|
|
119
|
+
img = proxy(data["bannerUrl"])
|
|
124
120
|
|
|
125
121
|
logger.debug(f"Fetching entries for contest: {contest}")
|
|
126
122
|
entries = get_entries(contest)
|
|
127
|
-
entry_count =
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
prizes_items = soup.select("article")
|
|
131
|
-
prizes = len(prizes_items) if prizes_items else 0
|
|
132
|
-
logger.debug(f"Found {prizes} prizes")
|
|
133
|
-
|
|
134
|
-
overview_section = soup.find("section", id="overview")
|
|
135
|
-
info = (
|
|
136
|
-
overview_section.decode_contents()
|
|
137
|
-
if overview_section
|
|
138
|
-
else "No Overview"
|
|
139
|
-
)
|
|
123
|
+
entry_count = data["numEntries"]
|
|
124
|
+
prizes = data["prizeCount"]
|
|
125
|
+
info = data.get("bodyFull", "")
|
|
140
126
|
|
|
141
127
|
except HTTPError as e:
|
|
142
128
|
logger.error(f"HTTP error fetching contest page: {e.code}")
|
|
@@ -147,11 +133,11 @@ def init_contest_routes(app):
|
|
|
147
133
|
doc = entry["document"]
|
|
148
134
|
entry_details = {
|
|
149
135
|
"link": url_for("route_article", article=doc["urlString"]),
|
|
150
|
-
"entry_img": doc["coverImageUrl"],
|
|
136
|
+
"entry_img": thumbnail(doc["coverImageUrl"]),
|
|
151
137
|
"entry_title": doc["title"],
|
|
152
138
|
"author": doc["screenName"],
|
|
153
139
|
"author_link": url_for("route_member", member=doc["screenName"]),
|
|
154
|
-
"channel": doc["channel"][0],
|
|
140
|
+
"channel": doc["channel"][0] if doc.get("channel") else "",
|
|
155
141
|
"channel_link": f"/{doc['primaryClassification']}",
|
|
156
142
|
"views": doc.get("views", 0),
|
|
157
143
|
}
|
|
@@ -173,16 +159,16 @@ def init_contest_routes(app):
|
|
|
173
159
|
|
|
174
160
|
try:
|
|
175
161
|
# Fetch current contests from the JSON API
|
|
176
|
-
|
|
177
|
-
|
|
162
|
+
data = get_cached_json(
|
|
163
|
+
app,
|
|
164
|
+
"https://www.instructables.com/json-api/getCurrentContests?limit=50&offset=0",
|
|
178
165
|
)
|
|
179
|
-
data = json.loads(response.read().decode())
|
|
180
166
|
logger.debug("Received current contests data")
|
|
181
167
|
except HTTPError as e:
|
|
182
168
|
logger.error(f"HTTP error fetching current contests: {e.code}")
|
|
183
169
|
abort(e.code)
|
|
184
|
-
except Exception as e:
|
|
185
|
-
logger.error(f"Error fetching current contests: {
|
|
170
|
+
except Exception as e: # noqa: BLE001
|
|
171
|
+
logger.error(f"Error fetching current contests: {e!s}")
|
|
186
172
|
abort(500) # Handle other exceptions such as JSON decode errors
|
|
187
173
|
|
|
188
174
|
contests = data.get("contests", [])
|