structables 0.4.3__tar.gz → 0.4.5__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.3 → structables-0.4.5}/.dockerignore +2 -0
  2. structables-0.4.5/.forgejo/workflows/docker.yml +38 -0
  3. {structables-0.4.3 → structables-0.4.5}/Dockerfile +4 -1
  4. {structables-0.4.3 → structables-0.4.5}/PKG-INFO +4 -3
  5. {structables-0.4.3 → structables-0.4.5}/README.md +0 -1
  6. {structables-0.4.3 → structables-0.4.5}/instances.json +0 -10
  7. {structables-0.4.3 → structables-0.4.5}/pyproject.toml +6 -1
  8. structables-0.4.5/scripts/create_release.py +153 -0
  9. {structables-0.4.3 → structables-0.4.5}/src/structables/routes/category.py +4 -0
  10. {structables-0.4.3 → structables-0.4.5}/src/structables/routes/contest.py +19 -37
  11. {structables-0.4.3 → structables-0.4.5}/src/structables/routes/main.py +126 -101
  12. structables-0.4.5/src/structables/routes/member.py +91 -0
  13. {structables-0.4.3 → structables-0.4.5}/src/structables/routes/proxy.py +28 -31
  14. {structables-0.4.3 → structables-0.4.5}/src/structables/static/css/style.css +163 -47
  15. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/archives.html +1 -1
  16. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/article.html +12 -8
  17. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/category.html +2 -2
  18. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/collection.html +2 -0
  19. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/contest.html +3 -3
  20. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/contests.html +1 -1
  21. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/index.html +1 -1
  22. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/member-instructables.html +13 -4
  23. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/member.html +8 -6
  24. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/projects.html +1 -1
  25. structables-0.4.5/src/structables/templates/sitemap.html +19 -0
  26. structables-0.4.5/src/structables/utils/cache.py +23 -0
  27. structables-0.4.5/src/structables/utils/data.py +87 -0
  28. {structables-0.4.3 → structables-0.4.5}/src/structables/utils/helpers.py +62 -101
  29. structables-0.4.3/.forgejo/workflows/docker.yml +0 -31
  30. structables-0.4.3/src/structables/routes/member.py +0 -166
  31. structables-0.4.3/src/structables/templates/sitemap.html +0 -27
  32. structables-0.4.3/src/structables/utils/data.py +0 -99
  33. {structables-0.4.3 → structables-0.4.5}/.env.example +0 -0
  34. {structables-0.4.3 → structables-0.4.5}/.forgejo/workflows/release.yml +0 -0
  35. {structables-0.4.3 → structables-0.4.5}/.gitignore +0 -0
  36. {structables-0.4.3 → structables-0.4.5}/LICENSE +0 -0
  37. {structables-0.4.3 → structables-0.4.5}/cronjob.txt +0 -0
  38. {structables-0.4.3 → structables-0.4.5}/docker-compose-example-dev.yml +0 -0
  39. {structables-0.4.3 → structables-0.4.5}/docker-compose-example.yml +0 -0
  40. {structables-0.4.3 → structables-0.4.5}/entrypoint.sh +0 -0
  41. {structables-0.4.3 → structables-0.4.5}/renovate.json +0 -0
  42. {structables-0.4.3 → structables-0.4.5}/requirements-dev.txt +0 -0
  43. {structables-0.4.3 → structables-0.4.5}/src/structables/__init__.py +0 -0
  44. {structables-0.4.3 → structables-0.4.5}/src/structables/config.py +0 -0
  45. {structables-0.4.3 → structables-0.4.5}/src/structables/main.py +0 -0
  46. {structables-0.4.3 → structables-0.4.5}/src/structables/routes/__init__.py +0 -0
  47. {structables-0.4.3 → structables-0.4.5}/src/structables/static/css/iframe.css +0 -0
  48. {structables-0.4.3 → structables-0.4.5}/src/structables/static/img/logo.png +0 -0
  49. {structables-0.4.3 → structables-0.4.5}/src/structables/static/img/logo_lg.png +0 -0
  50. {structables-0.4.3 → structables-0.4.5}/src/structables/static/img/magnifying-glass-solid.svg +0 -0
  51. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/400.html +0 -0
  52. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/404.html +0 -0
  53. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/429.html +0 -0
  54. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/500.html +0 -0
  55. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/article-review.html +0 -0
  56. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/base.html +0 -0
  57. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/footer.html +0 -0
  58. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/header.html +0 -0
  59. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/iframe.html +0 -0
  60. {structables-0.4.3 → structables-0.4.5}/src/structables/templates/privacypolicy.html +0 -0
  61. {structables-0.4.3 → structables-0.4.5}/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,38 @@
1
+ name: Docker CI/CD
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "*"
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ docker:
11
+ name: Docker Build and Push to git.private.coffee
12
+ runs-on: docker-privileged
13
+ container:
14
+ image: node:22-bookworm
15
+ options: >-
16
+ --privileged
17
+ steps:
18
+ - name: Install dependencies
19
+ run: |
20
+ apt update
21
+ apt install -y docker.io
22
+ - name: Set up QEMU
23
+ uses: docker/setup-qemu-action@v3
24
+ - name: Set up Docker Buildx
25
+ uses: docker/setup-buildx-action@v3
26
+ - 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 }}
32
+ - name: Build and push to Forgejo
33
+ uses: docker/build-push-action@v6
34
+ with:
35
+ push: true
36
+ tags: |
37
+ git.private.coffee/privatecoffee/structables:latest
38
+ 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.3
3
+ Version: 0.4.5
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.3"
7
+ version = "0.4.5"
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
@@ -58,6 +58,10 @@ def init_category_routes(app):
58
58
  logger.debug("Rendering craft category page")
59
59
  return category_page(app, "Craft")
60
60
 
61
+ @app.route("/design/")
62
+ def route_design():
63
+ return category_page(app, "Design")
64
+
61
65
  @app.route("/cooking/")
62
66
  def route_cooking():
63
67
  logger.debug("Rendering cooking category page")
@@ -1,9 +1,8 @@
1
1
  from flask import render_template, request, abort, url_for
2
- from urllib.request import urlopen, Request
3
2
  from urllib.error import HTTPError
4
- from ..utils.helpers import proxy
5
- from bs4 import BeautifulSoup
6
- import json
3
+ from urllib.parse import urlencode
4
+ from ..utils.cache import get_cached_json
5
+ from ..utils.helpers import proxy, thumbnail
7
6
  import logging
8
7
 
9
8
  logger = logging.getLogger(__name__)
@@ -20,11 +19,9 @@ def init_contest_routes(app):
20
19
  logger.debug(f"Fetching contest archive page {page} with limit {limit}")
21
20
 
22
21
  try:
23
- # Fetch data using urlopen
24
22
  url = f"https://www.instructables.com/json-api/getClosedContests?limit={limit}&offset={offset}"
25
23
  logger.debug(f"Making request to {url}")
26
- response = urlopen(url)
27
- data = json.loads(response.read().decode())
24
+ data = get_cached_json(app, url)
28
25
  logger.debug(
29
26
  f"Received contest archive data with {len(data.get('contests', []))} contests"
30
27
  )
@@ -85,9 +82,7 @@ def init_contest_routes(app):
85
82
  try:
86
83
  url = f"{base_url}?q=*&filter_by=contestPath:{contest}&sort_by=contestEntryDate:desc&per_page={per_page}&page={page}"
87
84
  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())
85
+ data = get_cached_json(app, url, headers=headers)
91
86
  except HTTPError as e:
92
87
  logger.error(f"HTTP error fetching contest entries: {e.code}")
93
88
  abort(e.code)
@@ -99,7 +94,7 @@ def init_contest_routes(app):
99
94
  break
100
95
 
101
96
  all_entries.extend(hits)
102
- if len(hits) < per_page:
97
+ if len(all_entries) >= data["found"]:
103
98
  break
104
99
  page += 1
105
100
 
@@ -111,32 +106,20 @@ def init_contest_routes(app):
111
106
  logger.debug(f"Fetching contest page for: {contest}")
112
107
 
113
108
  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"
109
+ data = get_cached_json(
110
+ app,
111
+ "https://www.instructables.com/json-api/contest?" + urlencode({"path": contest}),
112
+ )
113
+ title = data["title"]
120
114
  logger.debug(f"Contest title: {title}")
121
115
 
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"
116
+ img = proxy(data["bannerUrl"])
124
117
 
125
118
  logger.debug(f"Fetching entries for contest: {contest}")
126
119
  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
- )
120
+ entry_count = data["numEntries"]
121
+ prizes = data["prizeCount"]
122
+ info = data.get("bodyFull", "")
140
123
 
141
124
  except HTTPError as e:
142
125
  logger.error(f"HTTP error fetching contest page: {e.code}")
@@ -147,11 +130,11 @@ def init_contest_routes(app):
147
130
  doc = entry["document"]
148
131
  entry_details = {
149
132
  "link": url_for("route_article", article=doc["urlString"]),
150
- "entry_img": doc["coverImageUrl"],
133
+ "entry_img": thumbnail(doc["coverImageUrl"]),
151
134
  "entry_title": doc["title"],
152
135
  "author": doc["screenName"],
153
136
  "author_link": url_for("route_member", member=doc["screenName"]),
154
- "channel": doc["channel"][0],
137
+ "channel": doc["channel"][0] if doc.get("channel") else "",
155
138
  "channel_link": f"/{doc['primaryClassification']}",
156
139
  "views": doc.get("views", 0),
157
140
  }
@@ -173,10 +156,9 @@ def init_contest_routes(app):
173
156
 
174
157
  try:
175
158
  # Fetch current contests from the JSON API
176
- response = urlopen(
177
- "https://www.instructables.com/json-api/getCurrentContests?limit=50&offset=0"
159
+ data = get_cached_json(
160
+ app, "https://www.instructables.com/json-api/getCurrentContests?limit=50&offset=0"
178
161
  )
179
- data = json.loads(response.read().decode())
180
162
  logger.debug("Received current contests data")
181
163
  except HTTPError as e:
182
164
  logger.error(f"HTTP error fetching current contests: {e.code}")