structables 0.4.5__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 (57) hide show
  1. {structables-0.4.5 → structables-0.4.6}/.forgejo/workflows/docker.yml +27 -6
  2. {structables-0.4.5 → structables-0.4.6}/PKG-INFO +1 -1
  3. {structables-0.4.5 → structables-0.4.6}/pyproject.toml +1 -1
  4. {structables-0.4.5 → structables-0.4.6}/src/structables/config.py +11 -10
  5. {structables-0.4.5 → structables-0.4.6}/src/structables/main.py +1 -3
  6. {structables-0.4.5 → structables-0.4.6}/src/structables/routes/__init__.py +4 -3
  7. {structables-0.4.5 → structables-0.4.6}/src/structables/routes/category.py +4 -2
  8. {structables-0.4.5 → structables-0.4.6}/src/structables/routes/contest.py +12 -8
  9. {structables-0.4.5 → structables-0.4.6}/src/structables/routes/main.py +43 -29
  10. {structables-0.4.5 → structables-0.4.6}/src/structables/routes/member.py +10 -4
  11. {structables-0.4.5 → structables-0.4.6}/src/structables/routes/proxy.py +45 -26
  12. {structables-0.4.5 → structables-0.4.6}/src/structables/utils/data.py +21 -19
  13. {structables-0.4.5 → structables-0.4.6}/src/structables/utils/helpers.py +88 -63
  14. {structables-0.4.5 → structables-0.4.6}/.dockerignore +0 -0
  15. {structables-0.4.5 → structables-0.4.6}/.env.example +0 -0
  16. {structables-0.4.5 → structables-0.4.6}/.forgejo/workflows/release.yml +0 -0
  17. {structables-0.4.5 → structables-0.4.6}/.gitignore +0 -0
  18. {structables-0.4.5 → structables-0.4.6}/Dockerfile +0 -0
  19. {structables-0.4.5 → structables-0.4.6}/LICENSE +0 -0
  20. {structables-0.4.5 → structables-0.4.6}/README.md +0 -0
  21. {structables-0.4.5 → structables-0.4.6}/cronjob.txt +0 -0
  22. {structables-0.4.5 → structables-0.4.6}/docker-compose-example-dev.yml +0 -0
  23. {structables-0.4.5 → structables-0.4.6}/docker-compose-example.yml +0 -0
  24. {structables-0.4.5 → structables-0.4.6}/entrypoint.sh +0 -0
  25. {structables-0.4.5 → structables-0.4.6}/instances.json +0 -0
  26. {structables-0.4.5 → structables-0.4.6}/renovate.json +0 -0
  27. {structables-0.4.5 → structables-0.4.6}/requirements-dev.txt +0 -0
  28. {structables-0.4.5 → structables-0.4.6}/scripts/create_release.py +0 -0
  29. {structables-0.4.5 → structables-0.4.6}/src/structables/__init__.py +0 -0
  30. {structables-0.4.5 → structables-0.4.6}/src/structables/static/css/iframe.css +0 -0
  31. {structables-0.4.5 → structables-0.4.6}/src/structables/static/css/style.css +0 -0
  32. {structables-0.4.5 → structables-0.4.6}/src/structables/static/img/logo.png +0 -0
  33. {structables-0.4.5 → structables-0.4.6}/src/structables/static/img/logo_lg.png +0 -0
  34. {structables-0.4.5 → structables-0.4.6}/src/structables/static/img/magnifying-glass-solid.svg +0 -0
  35. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/400.html +0 -0
  36. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/404.html +0 -0
  37. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/429.html +0 -0
  38. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/500.html +0 -0
  39. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/archives.html +0 -0
  40. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/article-review.html +0 -0
  41. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/article.html +0 -0
  42. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/base.html +0 -0
  43. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/category.html +0 -0
  44. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/collection.html +0 -0
  45. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/contest.html +0 -0
  46. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/contests.html +0 -0
  47. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/footer.html +0 -0
  48. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/header.html +0 -0
  49. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/iframe.html +0 -0
  50. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/index.html +0 -0
  51. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/member-instructables.html +0 -0
  52. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/member.html +0 -0
  53. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/privacypolicy.html +0 -0
  54. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/projects.html +0 -0
  55. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/sitemap.html +0 -0
  56. {structables-0.4.5 → structables-0.4.6}/src/structables/templates/style.html +0 -0
  57. {structables-0.4.5 → structables-0.4.6}/src/structables/utils/cache.py +0 -0
@@ -6,6 +6,8 @@ on:
6
6
  - "*"
7
7
  workflow_dispatch:
8
8
 
9
+ enable-openid-connect: true
10
+
9
11
  jobs:
10
12
  docker:
11
13
  name: Docker Build and Push to git.private.coffee
@@ -15,20 +17,39 @@ jobs:
15
17
  options: >-
16
18
  --privileged
17
19
  steps:
20
+ - name: Check out code
21
+ uses: actions/checkout@v4
22
+
18
23
  - name: Install dependencies
19
24
  run: |
20
25
  apt update
21
- apt install -y docker.io
26
+ apt install -y docker.io jq
27
+
22
28
  - name: Set up QEMU
23
29
  uses: docker/setup-qemu-action@v3
30
+
24
31
  - name: Set up Docker Buildx
25
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
+
26
48
  - name: Login to Forgejo
27
- uses: docker/login-action@v3
28
- with:
29
- registry: git.private.coffee
30
- username: ${{ secrets.FORGEJO_USER }}
31
- password: ${{ secrets.FORGEJO_TOKEN }}
49
+ run: |
50
+ echo "${{ steps.jwt.outputs.jwt }}" \
51
+ | docker login --username token --password-stdin git.private.coffee
52
+
32
53
  - name: Build and push to Forgejo
33
54
  uses: docker/build-push-action@v6
34
55
  with:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: structables
3
- Version: 0.4.5
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "structables"
7
- version = "0.4.5"
7
+ version = "0.4.6"
8
8
  authors = [
9
9
  { name="Private.coffee Team", email="support@private.coffee" },
10
10
  ]
@@ -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
 
@@ -1,9 +1,11 @@
1
- from flask import render_template, request, abort, url_for
1
+ import logging
2
2
  from urllib.error import HTTPError
3
3
  from urllib.parse import urlencode
4
+
5
+ from flask import abort, render_template, request, url_for
6
+
4
7
  from ..utils.cache import get_cached_json
5
8
  from ..utils.helpers import proxy, thumbnail
6
- import logging
7
9
 
8
10
  logger = logging.getLogger(__name__)
9
11
 
@@ -28,8 +30,8 @@ def init_contest_routes(app):
28
30
  except HTTPError as e:
29
31
  logger.error(f"HTTP error fetching contest archive: {e.code}")
30
32
  abort(e.code)
31
- except Exception as e:
32
- 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}")
33
35
  abort(500) # Handle other exceptions like JSON decode errors
34
36
 
35
37
  contests = data.get("contests", [])
@@ -108,7 +110,8 @@ def init_contest_routes(app):
108
110
  try:
109
111
  data = get_cached_json(
110
112
  app,
111
- "https://www.instructables.com/json-api/contest?" + urlencode({"path": contest}),
113
+ "https://www.instructables.com/json-api/contest?"
114
+ + urlencode({"path": contest}),
112
115
  )
113
116
  title = data["title"]
114
117
  logger.debug(f"Contest title: {title}")
@@ -157,14 +160,15 @@ def init_contest_routes(app):
157
160
  try:
158
161
  # Fetch current contests from the JSON API
159
162
  data = get_cached_json(
160
- app, "https://www.instructables.com/json-api/getCurrentContests?limit=50&offset=0"
163
+ app,
164
+ "https://www.instructables.com/json-api/getCurrentContests?limit=50&offset=0",
161
165
  )
162
166
  logger.debug("Received current contests data")
163
167
  except HTTPError as e:
164
168
  logger.error(f"HTTP error fetching current contests: {e.code}")
165
169
  abort(e.code)
166
- except Exception as e:
167
- 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}")
168
172
  abort(500) # Handle other exceptions such as JSON decode errors
169
173
 
170
174
  contests = data.get("contests", [])
@@ -1,19 +1,24 @@
1
- from flask import render_template, abort, request
1
+ import logging
2
+ import pathlib
3
+ from traceback import print_exc
2
4
  from urllib.error import HTTPError
3
- from bs4 import BeautifulSoup
4
5
  from urllib.parse import quote
5
- from werkzeug.exceptions import InternalServerError
6
- from markdown2 import Markdown
7
- from traceback import print_exc
8
6
 
9
- import pathlib
10
- import logging
7
+ from bs4 import BeautifulSoup
8
+ from flask import abort, render_template, request
9
+ from markdown2 import Markdown
10
+ from werkzeug.exceptions import InternalServerError
11
11
 
12
12
  from ..utils.cache import get_cached_json
13
- from ..utils.data import update_data
14
- from ..utils.helpers import explore_lists, proxy, project_facets, projects_search, unslugify, thumbnail
13
+ from ..utils.data import maybe_update_data, update_data
14
+ from ..utils.helpers import (
15
+ project_facets,
16
+ projects_search,
17
+ proxy,
18
+ thumbnail,
19
+ unslugify,
20
+ )
15
21
  from .category import project_list
16
- from ..utils.data import maybe_update_data
17
22
 
18
23
  logger = logging.getLogger(__name__)
19
24
 
@@ -60,17 +65,15 @@ def init_main_routes(app):
60
65
  if "downloadUrl" in item
61
66
  else ""
62
67
  ),
63
- "author": item["author"] if "author" in item else "",
68
+ "author": item.get("author", ""),
64
69
  "channel": (
65
70
  item["classifications"][0]["channels"][0]["title"]
66
- if "classifications" in item
67
- and item["classifications"]
71
+ if item.get("classifications")
68
72
  else ""
69
73
  ),
70
74
  "channel_link": (
71
75
  f"/{item['classifications'][0]['name']}/{item['classifications'][0]['channels'][0]['name']}/"
72
- if "classifications" in item
73
- and item["classifications"]
76
+ if item.get("classifications")
74
77
  else ""
75
78
  ),
76
79
  }
@@ -84,14 +87,14 @@ def init_main_routes(app):
84
87
  )
85
88
 
86
89
  except Exception as e:
87
- logger.error(f"Error processing category {category}: {str(e)}")
90
+ logger.error(f"Error processing category {category}: {e!s}")
88
91
  raise
89
92
  sections.append((category.capitalize(), f"/{category}", []))
90
93
 
91
94
  return render_template("index.html", title="Explore", sections=sections)
92
95
 
93
- except Exception as e:
94
- logger.error(f"Error rendering explore page: {str(e)}")
96
+ except Exception as e: # noqa: BLE001
97
+ logger.error(f"Error rendering explore page: {e!s}")
95
98
  print_exc()
96
99
 
97
100
  # Fallback to a static version if API fails
@@ -122,7 +125,9 @@ def init_main_routes(app):
122
125
  try:
123
126
  categories = project_facets(app)["category"]
124
127
  if parts:
125
- categories = [name for name in categories if name.lower() == parts[0].lower()]
128
+ categories = [
129
+ name for name in categories if name.lower() == parts[0].lower()
130
+ ]
126
131
  if not categories:
127
132
  abort(404)
128
133
 
@@ -131,21 +136,31 @@ def init_main_routes(app):
131
136
  category_path = f"/{category.lower()}/"
132
137
  if len(parts) == 2:
133
138
  for channel in unslugify(parts[1]):
134
- hits, _ = projects_search(app, category=category, channel=channel)
139
+ hits, _ = projects_search(
140
+ app, category=category, channel=channel
141
+ )
135
142
  if hits:
136
143
  break
137
144
  channels = [
138
- [hit["document"]["title"], f'/{hit["document"]["urlString"]}']
145
+ [hit["document"]["title"], f"/{hit['document']['urlString']}"]
139
146
  for hit in hits
140
147
  ]
141
148
  else:
142
- channels = [[f"All {category} Projects", category_path + "projects/"]]
149
+ channels = [
150
+ [f"All {category} Projects", category_path + "projects/"]
151
+ ]
143
152
  for channel in project_facets(app, category)["channel"]:
144
153
  slug = quote(channel.lower().replace(" ", "-"), safe="")
145
154
  channels.append([channel, f"{category_path}{slug}/projects/"])
146
155
  groups.append([category, category_path, channels])
147
156
  if not parts:
148
- groups.append(["Teachers", "/teachers/", [["All Teachers Projects", "/teachers/projects/"]]])
157
+ groups.append(
158
+ [
159
+ "Teachers",
160
+ "/teachers/",
161
+ [["All Teachers Projects", "/teachers/projects/"]],
162
+ ]
163
+ )
149
164
  except HTTPError as e:
150
165
  logger.error(f"HTTP error fetching sitemap: {e.code}")
151
166
  abort(e.code)
@@ -217,7 +232,7 @@ def init_main_routes(app):
217
232
 
218
233
  for file in step["files"]:
219
234
  if file["image"]:
220
- if "embedType" not in "file":
235
+ if "embedType" not in file:
221
236
  step_imgs.append(
222
237
  {
223
238
  "src": thumbnail(file["downloadUrl"], 1024),
@@ -274,7 +289,7 @@ def init_main_routes(app):
274
289
  )
275
290
 
276
291
  elif not file["image"]:
277
- if "downloadUrl" in file.keys():
292
+ if "downloadUrl" in file:
278
293
  step_downloads.append(
279
294
  {
280
295
  "src": proxy(file["downloadUrl"], file["name"]),
@@ -412,8 +427,8 @@ def init_main_routes(app):
412
427
  thumbnails=thumbnails,
413
428
  )
414
429
 
415
- except Exception as e:
416
- logger.error(f"Error processing article: {str(e)}")
430
+ except Exception as e: # noqa: BLE001
431
+ logger.error(f"Error processing article: {e!s}")
417
432
  print_exc()
418
433
  raise InternalServerError()
419
434
 
@@ -468,8 +483,7 @@ def init_main_routes(app):
468
483
  content = Markdown().convert(content)
469
484
 
470
485
  except OSError as e:
471
- logger.error(f"Error reading privacy policy file: {str(e)}")
472
- pass
486
+ logger.error(f"Error reading privacy policy file: {e!s}")
473
487
 
474
488
  return render_template(
475
489
  "privacypolicy.html", title="Privacy Policy", content=content
@@ -1,9 +1,11 @@
1
- from flask import render_template, abort, request
1
+ import logging
2
2
  from urllib.error import HTTPError
3
3
  from urllib.parse import quote
4
+
5
+ from flask import abort, render_template, request
6
+
4
7
  from ..utils.cache import get_cached_json
5
- from ..utils.helpers import member_header, thumbnail, projects_search, get_pagination
6
- import logging
8
+ from ..utils.helpers import get_pagination, member_header, projects_search, thumbnail
7
9
 
8
10
  logger = logging.getLogger(__name__)
9
11
 
@@ -84,7 +86,11 @@ def init_member_routes(app):
84
86
  "member.html",
85
87
  title=header_content["title"] + "'s Profile",
86
88
  header_content=header_content,
87
- ible_list_title=("Top Instructables" if top_projects else "Recent Instructables") if ibles else "",
89
+ ible_list_title=(
90
+ "Top Instructables" if top_projects else "Recent Instructables"
91
+ )
92
+ if ibles
93
+ else "",
88
94
  ibles=ibles,
89
95
  ach_list_title="Contest Achievements" if achs else "",
90
96
  achs=achs,
@@ -1,14 +1,15 @@
1
- from flask import render_template, request, Response, abort
2
- from werkzeug.exceptions import BadRequest
3
- from werkzeug.http import dump_options_header
4
- from urllib.error import HTTPError
5
- from urllib.request import Request, urlopen
6
- from urllib.parse import quote
1
+ import hashlib
7
2
  import logging
8
3
  import os
9
- import hashlib
10
- import time
11
4
  import shutil
5
+ import time
6
+ from urllib.error import HTTPError
7
+ from urllib.parse import quote
8
+ from urllib.request import Request, urlopen
9
+
10
+ from flask import Response, abort, render_template, request
11
+ from werkzeug.exceptions import BadRequest
12
+ from werkzeug.http import dump_options_header
12
13
 
13
14
  logger = logging.getLogger(__name__)
14
15
 
@@ -196,8 +197,8 @@ def maybe_cleanup_cache(app):
196
197
  f"Cache cleanup complete. New size: {total_size / (1024 * 1024):.2f} MB"
197
198
  )
198
199
 
199
- except Exception as e:
200
- logger.error(f"Error during cache cleanup: {str(e)}")
200
+ except Exception as e: # noqa: BLE001
201
+ logger.error(f"Error during cache cleanup: {e!s}")
201
202
 
202
203
 
203
204
  def init_proxy_routes(app):
@@ -227,20 +228,30 @@ def init_proxy_routes(app):
227
228
  maybe_cleanup_cache(app)
228
229
 
229
230
  if url is not None:
230
- if url.startswith((
231
- "https://cdn.instructables.com/",
232
- "https://content.instructables.com/",
233
- "https://www.instructables.com/static/defaultIMG/",
234
- )):
231
+ if url.startswith(
232
+ (
233
+ "https://cdn.instructables.com/",
234
+ "https://content.instructables.com/",
235
+ "https://www.instructables.com/static/defaultIMG/",
236
+ )
237
+ ):
235
238
  logger.debug(f"Valid proxy URL: {url}")
236
239
  unquoted_url = url
237
240
 
238
- headers = {"Cache-Control": f"public, max-age={app.config['CACHE_MAX_AGE']}"}
241
+ headers = {
242
+ "Cache-Control": f"public, max-age={app.config['CACHE_MAX_AGE']}"
243
+ }
239
244
  if filename is not None:
240
- options = {"filename": filename} if filename.isascii() else {
241
- "filename*": "UTF-8''" + quote(filename, safe=""),
242
- }
243
- headers["Content-Disposition"] = dump_options_header("attachment", options)
245
+ options = (
246
+ {"filename": filename}
247
+ if filename.isascii()
248
+ else {
249
+ "filename*": "UTF-8''" + quote(filename, safe=""),
250
+ }
251
+ )
252
+ headers["Content-Disposition"] = dump_options_header(
253
+ "attachment", options
254
+ )
244
255
 
245
256
  # Check if the content is already cached
246
257
  if is_cached(app, unquoted_url):
@@ -323,16 +334,24 @@ def init_proxy_routes(app):
323
334
  except HTTPError as e:
324
335
  logger.error(f"HTTP error during streaming: {e.code}")
325
336
  abort(e.code)
326
- except Exception as e:
327
- logger.error(f"Error fetching content: {str(e)}")
337
+ except Exception as e: # noqa: BLE001
338
+ logger.error(f"Error fetching content: {e!s}")
328
339
  abort(500)
329
340
 
330
341
  try:
331
342
  logger.debug(f"Getting content type for {unquoted_url}")
332
- data = urlopen(Request(unquoted_url, headers={
333
- "Referer": "https://www.instructables.com/",
334
- }), timeout=30)
335
- content_type = data.headers.get("content-type", "application/octet-stream")
343
+ data = urlopen(
344
+ Request(
345
+ unquoted_url,
346
+ headers={
347
+ "Referer": "https://www.instructables.com/",
348
+ },
349
+ ),
350
+ timeout=30,
351
+ )
352
+ content_type = data.headers.get(
353
+ "content-type", "application/octet-stream"
354
+ )
336
355
  logger.debug(f"Content type: {content_type}")
337
356
  except HTTPError as e:
338
357
  logger.error(f"HTTP error getting content type: {e.code}")
@@ -1,51 +1,51 @@
1
1
  import logging
2
2
  import time
3
- from .helpers import thumbnail, projects_search
3
+
4
+ from .helpers import projects_search, thumbnail
4
5
 
5
6
  logger = logging.getLogger(__name__)
6
7
 
7
8
  # Track the last data update time
8
9
  last_data_update = 0
9
10
 
11
+
10
12
  def update_data(app):
11
13
  """Update the application's cached data.
12
-
14
+
13
15
  This function fetches fresh data from Instructables.com and updates
14
16
  the app's global cache.
15
-
17
+
16
18
  Args:
17
19
  app: The Flask app instance.
18
20
  """
19
21
  logger.debug("Starting data update")
20
22
 
21
- try:
22
- app.global_ibles
23
- except AttributeError:
23
+ if not hasattr(app, "global_ibles"):
24
24
  logger.debug("Initializing global_ibles dictionary")
25
25
  app.global_ibles = {"/projects": []}
26
26
 
27
27
  try:
28
28
  logger.debug("Fetching featured projects")
29
29
  projects = []
30
- project_ibles, total = projects_search(app, filter_by="featureFlag:=true")
31
-
30
+ project_ibles, _total = projects_search(app, filter_by="featureFlag:=true")
31
+
32
32
  logger.debug(f"Found {len(project_ibles)} featured projects")
33
33
  if not project_ibles:
34
34
  return
35
35
 
36
36
  for ible in project_ibles:
37
37
  link = f"/{ible['document']['urlString']}"
38
- img = thumbnail(ible['document']['coverImageUrl'])
38
+ img = thumbnail(ible["document"]["coverImageUrl"])
39
39
 
40
- title = ible['document']['title']
41
- author = ible['document']['screenName']
40
+ title = ible["document"]["title"]
41
+ author = ible["document"]["screenName"]
42
42
  author_link = f"/member/{author}"
43
43
 
44
- channel = ible['document']['primaryClassification']
44
+ channel = ible["document"]["primaryClassification"]
45
45
  channel_link = f"/channel/{channel}"
46
46
 
47
- views = ible['document']['views']
48
- favorites = ible['document']['favorites']
47
+ views = ible["document"]["views"]
48
+ favorites = ible["document"]["favorites"]
49
49
 
50
50
  projects.append(
51
51
  {
@@ -62,10 +62,12 @@ def update_data(app):
62
62
  )
63
63
 
64
64
  app.global_ibles["/projects"] = projects
65
- logger.debug(f"Updated global projects list with {len(app.global_ibles['/projects'])} projects")
65
+ logger.debug(
66
+ f"Updated global projects list with {len(app.global_ibles['/projects'])} projects"
67
+ )
66
68
  logger.debug("Data update completed successfully")
67
- except Exception as e:
68
- logger.error(f"Error updating data: {str(e)}")
69
+ except Exception as e: # noqa: BLE001
70
+ logger.error(f"Error updating data: {e!s}")
69
71
 
70
72
 
71
73
  def maybe_update_data(app):
@@ -78,10 +80,10 @@ def maybe_update_data(app):
78
80
  """
79
81
  global last_data_update
80
82
  current_time = time.time()
81
-
83
+
82
84
  # Update every 5 minutes (300 seconds)
83
85
  if current_time - last_data_update >= 300:
84
86
  logger.debug("Running scheduled data update")
85
87
  update_data(app)
86
88
  last_data_update = current_time
87
- logger.debug("Data update complete")
89
+ logger.debug("Data update complete")
@@ -1,21 +1,24 @@
1
- from urllib.parse import urlencode, urlparse, quote, urlsplit, urlunsplit, parse_qsl
2
- from urllib.request import urlopen
3
- from bs4 import BeautifulSoup
4
- import re
5
1
  import logging
6
2
  import math
7
- from flask import request, render_template, abort
3
+ import re
4
+ from urllib.parse import parse_qsl, quote, urlencode, urlparse, urlsplit, urlunsplit
5
+ from urllib.request import urlopen
6
+
7
+ from bs4 import BeautifulSoup
8
+ from flask import abort, render_template, request
9
+
8
10
  from .cache import get_cached_json
9
11
 
10
12
  logger = logging.getLogger(__name__)
11
13
 
14
+
12
15
  def proxy(url, filename=None):
13
16
  """Generate a proxy URL for external content.
14
-
17
+
15
18
  Args:
16
19
  url (str): The original URL to proxy.
17
20
  filename (str, optional): The filename to use for downloads.
18
-
21
+
19
22
  Returns:
20
23
  str: The proxied URL.
21
24
  """
@@ -25,6 +28,7 @@ def proxy(url, filename=None):
25
28
  args["filename"] = filename
26
29
  return "/proxy/?" + urlencode(args)
27
30
 
31
+
28
32
  def thumbnail(url, width=600):
29
33
  parts = urlsplit(url)
30
34
  if parts.hostname in ("content.instructables.com", "cdn.instructables.com"):
@@ -36,7 +40,7 @@ def thumbnail(url, width=600):
36
40
 
37
41
  def get_typesense_api_key():
38
42
  """Extract the Typesense API key from Instructables.com.
39
-
43
+
40
44
  Returns:
41
45
  str: The Typesense API key.
42
46
  """
@@ -56,8 +60,9 @@ def get_typesense_api_key():
56
60
  return api_key
57
61
 
58
62
  logger.error("Failed to get Typesense API key")
59
- except Exception as e:
60
- logger.error(f"Error getting Typesense API key: {str(e)}")
63
+ except Exception as e: # noqa: BLE001
64
+ logger.error(f"Error getting Typesense API key: {e!s}")
65
+
61
66
 
62
67
  def unslugify(slug):
63
68
  """Return a list of possible original titles for a slug.
@@ -82,14 +87,15 @@ def unslugify(slug):
82
87
  logger.debug(f"Unslugify results: {results}")
83
88
  return results
84
89
 
90
+
85
91
  def get_pagination(request, total, per_page=1):
86
92
  """Generate pagination links.
87
-
93
+
88
94
  Args:
89
95
  request: The Flask request object.
90
96
  total (int): The total number of items.
91
97
  per_page (int): The number of items per page.
92
-
98
+
93
99
  Returns:
94
100
  list: A list of pagination link dictionaries.
95
101
  """
@@ -150,6 +156,7 @@ def get_pagination(request, total, per_page=1):
150
156
  logger.debug(f"Generated {len(pagination)} pagination links")
151
157
  return pagination
152
158
 
159
+
153
160
  def member_header(data):
154
161
  """Extract profile header information from the author API response."""
155
162
  signup = (data.get("signup") or "")[:10]
@@ -168,10 +175,10 @@ def member_header(data):
168
175
 
169
176
  def explore_lists(soup):
170
177
  """Parse the explore lists from the homepage.
171
-
178
+
172
179
  Args:
173
180
  soup: The BeautifulSoup element containing the list.
174
-
181
+
175
182
  Returns:
176
183
  list: A list of dictionaries with project information.
177
184
  """
@@ -208,20 +215,21 @@ def explore_lists(soup):
208
215
  }
209
216
  )
210
217
  logger.debug(f"Found {len(list_)} items in explore list")
211
- except Exception as e:
212
- logger.error(f"Error parsing explore list: {str(e)}")
213
-
218
+ except Exception as e: # noqa: BLE001
219
+ logger.error(f"Error parsing explore list: {e!s}")
220
+
214
221
  return list_
215
222
 
223
+
216
224
  def project_list(app, head, sort="", per_page=20):
217
225
  """Generate a list of projects for display.
218
-
226
+
219
227
  Args:
220
228
  app: The Flask app instance.
221
229
  head (str): The header title.
222
230
  sort (str, optional): Sort description.
223
231
  per_page (int, optional): Number of items per page.
224
-
232
+
225
233
  Returns:
226
234
  Response: The rendered template.
227
235
  """
@@ -244,7 +252,7 @@ def project_list(app, head, sort="", per_page=20):
244
252
  parts = path.split("/")
245
253
  category = "" if parts[1] == "projects" else parts[1]
246
254
  channel = "" if not category or parts[2] == "projects" else parts[2]
247
-
255
+
248
256
  logger.debug(f"Category: {category}, Channel: {channel}")
249
257
 
250
258
  channel_names = unslugify(channel)
@@ -261,7 +269,9 @@ def project_list(app, head, sort="", per_page=20):
261
269
  )
262
270
 
263
271
  if project_ibles:
264
- logger.debug(f"Found {len(project_ibles)} projects for {channel_name}")
272
+ logger.debug(
273
+ f"Found {len(project_ibles)} projects for {channel_name}"
274
+ )
265
275
  break
266
276
 
267
277
  elif "search" in path.split("/"):
@@ -288,17 +298,17 @@ def project_list(app, head, sort="", per_page=20):
288
298
 
289
299
  for ible in project_ibles:
290
300
  link = f"/{ible['document']['urlString']}"
291
- img = thumbnail(ible['document']['coverImageUrl'])
301
+ img = thumbnail(ible["document"]["coverImageUrl"])
292
302
 
293
- title = ible['document']['title']
294
- author = ible['document']['screenName']
303
+ title = ible["document"]["title"]
304
+ author = ible["document"]["screenName"]
295
305
  author_link = f"/member/{author}"
296
306
 
297
- channel = ible['document']['primaryClassification']
307
+ channel = ible["document"]["primaryClassification"]
298
308
  channel_link = f"/channel/{channel}"
299
309
 
300
- views = ible['document']['views']
301
- favorites = ible['document']['favorites']
310
+ views = ible["document"]["views"]
311
+ favorites = ible["document"]["favorites"]
302
312
 
303
313
  ibles.append(
304
314
  {
@@ -313,12 +323,12 @@ def project_list(app, head, sort="", per_page=20):
313
323
  "favorites": favorites,
314
324
  }
315
325
  )
316
-
326
+
317
327
  logger.debug(f"Processed {len(ibles)} projects for display")
318
328
 
319
329
  pagination = get_pagination(request, total, per_page)
320
330
  logger.debug(f"Rendering project list template with {len(ibles)} projects")
321
-
331
+
322
332
  return render_template(
323
333
  "projects.html",
324
334
  title=unslugify(head)[0],
@@ -327,14 +337,15 @@ def project_list(app, head, sort="", per_page=20):
327
337
  pagination=pagination,
328
338
  )
329
339
 
340
+
330
341
  def category_page(app, name, teachers=False):
331
342
  """Generate a category page.
332
-
343
+
333
344
  Args:
334
345
  app: The Flask app instance.
335
346
  name (str): The category name.
336
347
  teachers (bool, optional): Whether this is the teachers category.
337
-
348
+
338
349
  Returns:
339
350
  Response: The rendered template.
340
351
  """
@@ -353,36 +364,36 @@ def category_page(app, name, teachers=False):
353
364
  and channel["channel"] not in channels
354
365
  ):
355
366
  channels.append(channel["channel"])
356
-
367
+
357
368
  logger.debug(f"Found {len(channels)} channels for category {name}")
358
369
 
359
370
  # Get featured projects
360
371
  if teachers:
361
372
  logger.debug("Fetching teachers projects")
362
- category_ibles, total = projects_search(
373
+ category_ibles, _total = projects_search(
363
374
  app, teachers=True, page=page, filter_by="featureFlag:=true"
364
375
  )
365
376
  else:
366
377
  logger.debug(f"Fetching featured projects for category {name}")
367
- category_ibles, total = projects_search(
378
+ category_ibles, _total = projects_search(
368
379
  app, category=name, page=page, filter_by="featureFlag:=true"
369
380
  )
370
-
381
+
371
382
  logger.debug(f"Found {len(category_ibles)} featured projects")
372
383
 
373
384
  for ible in category_ibles:
374
385
  link = f"/{ible['document']['urlString']}"
375
- img = thumbnail(ible['document']['coverImageUrl'])
386
+ img = thumbnail(ible["document"]["coverImageUrl"])
376
387
 
377
- title = ible['document']['title']
378
- author = ible['document']['screenName']
388
+ title = ible["document"]["title"]
389
+ author = ible["document"]["screenName"]
379
390
  author_link = f"/member/{author}"
380
391
 
381
- channel = ible['document']['primaryClassification']
392
+ channel = ible["document"]["primaryClassification"]
382
393
  channel_link = f"/channel/{channel}"
383
394
 
384
- views = ible['document']['views']
385
- favorites = ible['document']['favorites']
395
+ views = ible["document"]["views"]
396
+ favorites = ible["document"]["favorites"]
386
397
 
387
398
  ibles.append(
388
399
  {
@@ -408,20 +419,28 @@ def category_page(app, name, teachers=False):
408
419
  path=path,
409
420
  )
410
421
 
422
+
411
423
  def project_facets(app, category=""):
412
- args = urlencode({
413
- "q": "*",
414
- "query_by": "title",
415
- "facet_by": "category,channel",
416
- "max_facet_values": 250,
417
- "per_page": 0,
418
- "include_fields": "id",
419
- "filter_by": f"category:={category}" if category else "",
420
- })
424
+ args = urlencode(
425
+ {
426
+ "q": "*",
427
+ "query_by": "title",
428
+ "facet_by": "category,channel",
429
+ "max_facet_values": 250,
430
+ "per_page": 0,
431
+ "include_fields": "id",
432
+ "filter_by": f"category:={category}" if category else "",
433
+ }
434
+ )
421
435
  url = f"https://www.instructables.com/api_proxy/search/collections/projects/documents/search?{args}"
422
- result = get_cached_json(app, url, headers={
423
- "x-typesense-api-key": app.config["TYPESENSE_API_KEY"],
424
- }, max_age=24 * 60 * 60)
436
+ result = get_cached_json(
437
+ app,
438
+ url,
439
+ headers={
440
+ "x-typesense-api-key": app.config["TYPESENSE_API_KEY"],
441
+ },
442
+ max_age=24 * 60 * 60,
443
+ )
425
444
  return {
426
445
  facet["field_name"]: sorted(item["value"] for item in facet["counts"])
427
446
  for facet in result["facet_counts"]
@@ -443,7 +462,7 @@ def projects_search(
443
462
  typesense_api_key=None,
444
463
  ):
445
464
  """Search for projects using the Typesense API.
446
-
465
+
447
466
  Args:
448
467
  app: The Flask app instance.
449
468
  query (str, optional): The search query.
@@ -457,7 +476,7 @@ def projects_search(
457
476
  sort_by (str, optional): The sort order.
458
477
  timeout (int, optional): The request timeout.
459
478
  typesense_api_key (str, optional): The Typesense API key.
460
-
479
+
461
480
  Returns:
462
481
  tuple: A tuple of (projects, total_pages).
463
482
  """
@@ -480,7 +499,9 @@ def projects_search(
480
499
  query = quote(query)
481
500
  filter_by = quote(filter_by)
482
501
 
483
- logger.debug(f"Searching projects: query='{query}', filter='{filter_by}', page={page}, per_page={per_page}")
502
+ logger.debug(
503
+ f"Searching projects: query='{query}', filter='{filter_by}', page={page}, per_page={per_page}"
504
+ )
484
505
 
485
506
  projects_headers = {"x-typesense-api-key": app.config["TYPESENSE_API_KEY"]}
486
507
 
@@ -498,15 +519,19 @@ def projects_search(
498
519
 
499
520
  url = f"https://www.instructables.com/api_proxy/search/collections/projects/documents/search?{args_str}"
500
521
  logger.debug(f"Making request to {url}")
501
-
522
+
502
523
  try:
503
- project_obj = get_cached_json(app, url, headers=projects_headers, timeout=timeout)
524
+ project_obj = get_cached_json(
525
+ app, url, headers=projects_headers, timeout=timeout
526
+ )
504
527
  project_ibles = project_obj["hits"]
505
528
  total_found = project_obj["found"]
506
-
507
- logger.debug(f"Search returned {len(project_ibles)} projects out of {total_found} total matches")
508
-
529
+
530
+ logger.debug(
531
+ f"Search returned {len(project_ibles)} projects out of {total_found} total matches"
532
+ )
533
+
509
534
  return project_ibles, math.ceil(total_found / per_page)
510
- except Exception as e:
511
- logger.error(f"Error searching projects: {str(e)}")
535
+ except Exception as e: # noqa: BLE001
536
+ logger.error(f"Error searching projects: {e!s}")
512
537
  return [], 0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes