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.
- {structables-0.4.5 → structables-0.4.6}/.forgejo/workflows/docker.yml +27 -6
- {structables-0.4.5 → structables-0.4.6}/PKG-INFO +1 -1
- {structables-0.4.5 → structables-0.4.6}/pyproject.toml +1 -1
- {structables-0.4.5 → structables-0.4.6}/src/structables/config.py +11 -10
- {structables-0.4.5 → structables-0.4.6}/src/structables/main.py +1 -3
- {structables-0.4.5 → structables-0.4.6}/src/structables/routes/__init__.py +4 -3
- {structables-0.4.5 → structables-0.4.6}/src/structables/routes/category.py +4 -2
- {structables-0.4.5 → structables-0.4.6}/src/structables/routes/contest.py +12 -8
- {structables-0.4.5 → structables-0.4.6}/src/structables/routes/main.py +43 -29
- {structables-0.4.5 → structables-0.4.6}/src/structables/routes/member.py +10 -4
- {structables-0.4.5 → structables-0.4.6}/src/structables/routes/proxy.py +45 -26
- {structables-0.4.5 → structables-0.4.6}/src/structables/utils/data.py +21 -19
- {structables-0.4.5 → structables-0.4.6}/src/structables/utils/helpers.py +88 -63
- {structables-0.4.5 → structables-0.4.6}/.dockerignore +0 -0
- {structables-0.4.5 → structables-0.4.6}/.env.example +0 -0
- {structables-0.4.5 → structables-0.4.6}/.forgejo/workflows/release.yml +0 -0
- {structables-0.4.5 → structables-0.4.6}/.gitignore +0 -0
- {structables-0.4.5 → structables-0.4.6}/Dockerfile +0 -0
- {structables-0.4.5 → structables-0.4.6}/LICENSE +0 -0
- {structables-0.4.5 → structables-0.4.6}/README.md +0 -0
- {structables-0.4.5 → structables-0.4.6}/cronjob.txt +0 -0
- {structables-0.4.5 → structables-0.4.6}/docker-compose-example-dev.yml +0 -0
- {structables-0.4.5 → structables-0.4.6}/docker-compose-example.yml +0 -0
- {structables-0.4.5 → structables-0.4.6}/entrypoint.sh +0 -0
- {structables-0.4.5 → structables-0.4.6}/instances.json +0 -0
- {structables-0.4.5 → structables-0.4.6}/renovate.json +0 -0
- {structables-0.4.5 → structables-0.4.6}/requirements-dev.txt +0 -0
- {structables-0.4.5 → structables-0.4.6}/scripts/create_release.py +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/__init__.py +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/static/css/iframe.css +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/static/css/style.css +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/static/img/logo.png +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/static/img/logo_lg.png +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/static/img/magnifying-glass-solid.svg +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/400.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/404.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/429.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/500.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/archives.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/article-review.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/article.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/base.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/category.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/collection.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/contest.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/contests.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/footer.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/header.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/iframe.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/index.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/member-instructables.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/member.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/privacypolicy.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/projects.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/sitemap.html +0 -0
- {structables-0.4.5 → structables-0.4.6}/src/structables/templates/style.html +0 -0
- {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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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.
|
|
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
|
|
@@ -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
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
|
|
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: {
|
|
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?"
|
|
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,
|
|
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: {
|
|
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
|
-
|
|
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
|
|
10
|
-
import
|
|
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
|
|
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
|
|
68
|
+
"author": item.get("author", ""),
|
|
64
69
|
"channel": (
|
|
65
70
|
item["classifications"][0]["channels"][0]["title"]
|
|
66
|
-
if "classifications"
|
|
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"
|
|
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}: {
|
|
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: {
|
|
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 = [
|
|
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(
|
|
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
|
|
145
|
+
[hit["document"]["title"], f"/{hit['document']['urlString']}"]
|
|
139
146
|
for hit in hits
|
|
140
147
|
]
|
|
141
148
|
else:
|
|
142
|
-
channels = [
|
|
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(
|
|
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
|
|
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
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
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
|
|
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=(
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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 = {
|
|
241
|
+
headers = {
|
|
242
|
+
"Cache-Control": f"public, max-age={app.config['CACHE_MAX_AGE']}"
|
|
243
|
+
}
|
|
239
244
|
if filename is not None:
|
|
240
|
-
options =
|
|
241
|
-
"filename
|
|
242
|
-
|
|
243
|
-
|
|
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: {
|
|
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(
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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[
|
|
38
|
+
img = thumbnail(ible["document"]["coverImageUrl"])
|
|
39
39
|
|
|
40
|
-
title = ible[
|
|
41
|
-
author = ible[
|
|
40
|
+
title = ible["document"]["title"]
|
|
41
|
+
author = ible["document"]["screenName"]
|
|
42
42
|
author_link = f"/member/{author}"
|
|
43
43
|
|
|
44
|
-
channel = ible[
|
|
44
|
+
channel = ible["document"]["primaryClassification"]
|
|
45
45
|
channel_link = f"/channel/{channel}"
|
|
46
46
|
|
|
47
|
-
views = ible[
|
|
48
|
-
favorites = ible[
|
|
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(
|
|
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: {
|
|
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
|
-
|
|
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: {
|
|
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: {
|
|
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(
|
|
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[
|
|
301
|
+
img = thumbnail(ible["document"]["coverImageUrl"])
|
|
292
302
|
|
|
293
|
-
title = ible[
|
|
294
|
-
author = ible[
|
|
303
|
+
title = ible["document"]["title"]
|
|
304
|
+
author = ible["document"]["screenName"]
|
|
295
305
|
author_link = f"/member/{author}"
|
|
296
306
|
|
|
297
|
-
channel = ible[
|
|
307
|
+
channel = ible["document"]["primaryClassification"]
|
|
298
308
|
channel_link = f"/channel/{channel}"
|
|
299
309
|
|
|
300
|
-
views = ible[
|
|
301
|
-
favorites = ible[
|
|
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,
|
|
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,
|
|
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[
|
|
386
|
+
img = thumbnail(ible["document"]["coverImageUrl"])
|
|
376
387
|
|
|
377
|
-
title = ible[
|
|
378
|
-
author = ible[
|
|
388
|
+
title = ible["document"]["title"]
|
|
389
|
+
author = ible["document"]["screenName"]
|
|
379
390
|
author_link = f"/member/{author}"
|
|
380
391
|
|
|
381
|
-
channel = ible[
|
|
392
|
+
channel = ible["document"]["primaryClassification"]
|
|
382
393
|
channel_link = f"/channel/{channel}"
|
|
383
394
|
|
|
384
|
-
views = ible[
|
|
385
|
-
favorites = ible[
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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(
|
|
423
|
-
|
|
424
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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: {
|
|
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
|
|
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
|
{structables-0.4.5 → structables-0.4.6}/src/structables/static/img/magnifying-glass-solid.svg
RENAMED
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|