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.
Files changed (61) hide show
  1. {structables-0.4.4 → structables-0.4.6}/.dockerignore +2 -0
  2. structables-0.4.6/.forgejo/workflows/docker.yml +59 -0
  3. {structables-0.4.4 → structables-0.4.6}/Dockerfile +4 -1
  4. {structables-0.4.4 → structables-0.4.6}/PKG-INFO +4 -3
  5. {structables-0.4.4 → structables-0.4.6}/README.md +0 -1
  6. {structables-0.4.4 → structables-0.4.6}/instances.json +0 -10
  7. {structables-0.4.4 → structables-0.4.6}/pyproject.toml +6 -1
  8. structables-0.4.6/scripts/create_release.py +153 -0
  9. {structables-0.4.4 → structables-0.4.6}/src/structables/config.py +11 -10
  10. {structables-0.4.4 → structables-0.4.6}/src/structables/main.py +1 -3
  11. {structables-0.4.4 → structables-0.4.6}/src/structables/routes/__init__.py +4 -3
  12. {structables-0.4.4 → structables-0.4.6}/src/structables/routes/category.py +8 -2
  13. {structables-0.4.4 → structables-0.4.6}/src/structables/routes/contest.py +29 -43
  14. {structables-0.4.4 → structables-0.4.6}/src/structables/routes/main.py +85 -95
  15. structables-0.4.6/src/structables/routes/member.py +97 -0
  16. {structables-0.4.4 → structables-0.4.6}/src/structables/routes/proxy.py +54 -38
  17. {structables-0.4.4 → structables-0.4.6}/src/structables/static/css/style.css +163 -47
  18. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/archives.html +1 -1
  19. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/article.html +12 -8
  20. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/category.html +2 -2
  21. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/collection.html +2 -0
  22. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/contest.html +3 -3
  23. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/contests.html +1 -1
  24. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/index.html +1 -1
  25. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/member-instructables.html +13 -4
  26. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/member.html +8 -6
  27. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/projects.html +1 -1
  28. structables-0.4.6/src/structables/templates/sitemap.html +19 -0
  29. structables-0.4.6/src/structables/utils/cache.py +23 -0
  30. structables-0.4.6/src/structables/utils/data.py +89 -0
  31. {structables-0.4.4 → structables-0.4.6}/src/structables/utils/helpers.py +132 -146
  32. structables-0.4.4/.forgejo/workflows/docker.yml +0 -37
  33. structables-0.4.4/src/structables/routes/member.py +0 -166
  34. structables-0.4.4/src/structables/templates/sitemap.html +0 -27
  35. structables-0.4.4/src/structables/utils/data.py +0 -99
  36. {structables-0.4.4 → structables-0.4.6}/.env.example +0 -0
  37. {structables-0.4.4 → structables-0.4.6}/.forgejo/workflows/release.yml +0 -0
  38. {structables-0.4.4 → structables-0.4.6}/.gitignore +0 -0
  39. {structables-0.4.4 → structables-0.4.6}/LICENSE +0 -0
  40. {structables-0.4.4 → structables-0.4.6}/cronjob.txt +0 -0
  41. {structables-0.4.4 → structables-0.4.6}/docker-compose-example-dev.yml +0 -0
  42. {structables-0.4.4 → structables-0.4.6}/docker-compose-example.yml +0 -0
  43. {structables-0.4.4 → structables-0.4.6}/entrypoint.sh +0 -0
  44. {structables-0.4.4 → structables-0.4.6}/renovate.json +0 -0
  45. {structables-0.4.4 → structables-0.4.6}/requirements-dev.txt +0 -0
  46. {structables-0.4.4 → structables-0.4.6}/src/structables/__init__.py +0 -0
  47. {structables-0.4.4 → structables-0.4.6}/src/structables/static/css/iframe.css +0 -0
  48. {structables-0.4.4 → structables-0.4.6}/src/structables/static/img/logo.png +0 -0
  49. {structables-0.4.4 → structables-0.4.6}/src/structables/static/img/logo_lg.png +0 -0
  50. {structables-0.4.4 → structables-0.4.6}/src/structables/static/img/magnifying-glass-solid.svg +0 -0
  51. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/400.html +0 -0
  52. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/404.html +0 -0
  53. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/429.html +0 -0
  54. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/500.html +0 -0
  55. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/article-review.html +0 -0
  56. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/base.html +0 -0
  57. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/footer.html +0 -0
  58. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/header.html +0 -0
  59. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/iframe.html +0 -0
  60. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/privacypolicy.html +0 -0
  61. {structables-0.4.4 → structables-0.4.6}/src/structables/templates/style.html +0 -0
@@ -1,7 +1,9 @@
1
1
  .env
2
+ .git
2
3
  .vscode
3
4
  __pycache__/
4
5
  docker-compose.yml
6
+ compose.yaml
5
7
  privacy.md
6
8
  privacy.txt
7
9
  *.pyc
@@ -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.4
1
+ Metadata-Version: 2.5
2
2
  Name: structables
3
- Version: 0.4.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.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", False))
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", False)
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(f"Could not create or write to cache directory: {e} - disabling cache")
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", 60 * 60 * 24 * 7)
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", 1024 * 1024 * 1024)
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", 60 * 60)
55
+ os.environ.get("STRUCTABLES_CACHE_CLEANUP_INTERVAL", "3600")
55
56
  ) # 1 hour default
56
57
 
57
58
  @staticmethod
@@ -1,8 +1,6 @@
1
- #!/usr/bin/env python
1
+ import logging
2
2
 
3
3
  from flask import Flask
4
- import logging
5
- import time
6
4
 
7
5
  from .config import Config
8
6
  from .routes import init_routes
@@ -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
- from .contest import init_contest_routes
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
- from ..utils.helpers import project_list, category_page
4
- import logging
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
- response = urlopen(url)
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: {str(e)}")
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
- request = Request(url, headers=headers)
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(hits) < per_page:
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 = urlopen(f"https://www.instructables.com/contest/{contest}/")
115
- html = data.read().decode()
116
- soup = BeautifulSoup(html, "html.parser")
117
-
118
- title_tag = soup.find("h1")
119
- title = title_tag.get_text() if title_tag else "Contest"
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
- img_tag = soup.find("img", alt=lambda x: x and "Banner" in x)
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 = len(entries)
128
- logger.debug(f"Found {entry_count} entries")
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
- response = urlopen(
177
- "https://www.instructables.com/json-api/getCurrentContests?limit=50&offset=0"
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: {str(e)}")
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", [])