structables 0.3.16__tar.gz → 0.4.0__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 (70) hide show
  1. {structables-0.3.16 → structables-0.4.0}/.forgejo/workflows/docker.yml +2 -2
  2. {structables-0.3.16 → structables-0.4.0}/.forgejo/workflows/release.yml +1 -1
  3. {structables-0.3.16 → structables-0.4.0}/.gitignore +1 -0
  4. {structables-0.3.16 → structables-0.4.0}/Dockerfile +1 -1
  5. {structables-0.3.16 → structables-0.4.0}/PKG-INFO +24 -10
  6. {structables-0.3.16 → structables-0.4.0}/README.md +23 -9
  7. structables-0.4.0/instances.json +54 -0
  8. {structables-0.3.16 → structables-0.4.0}/pyproject.toml +1 -1
  9. structables-0.4.0/renovate.json +6 -0
  10. structables-0.4.0/requirements-dev.txt +6 -0
  11. structables-0.4.0/src/structables/config.py +44 -0
  12. structables-0.4.0/src/structables/main.py +65 -0
  13. {structables-0.3.16 → structables-0.4.0}/src/structables/routes/category.py +21 -0
  14. {structables-0.3.16 → structables-0.4.0}/src/structables/routes/contest.py +39 -4
  15. {structables-0.3.16 → structables-0.4.0}/src/structables/routes/main.py +125 -49
  16. {structables-0.3.16 → structables-0.4.0}/src/structables/routes/member.py +22 -3
  17. structables-0.4.0/src/structables/routes/proxy.py +358 -0
  18. structables-0.4.0/src/structables/static/css/iframe.css +114 -0
  19. structables-0.4.0/src/structables/static/css/style.css +1190 -0
  20. structables-0.4.0/src/structables/static/img/magnifying-glass-solid.svg +2 -0
  21. structables-0.4.0/src/structables/templates/base.html +58 -0
  22. structables-0.4.0/src/structables/templates/category.html +35 -0
  23. structables-0.4.0/src/structables/templates/footer.html +17 -0
  24. structables-0.4.0/src/structables/templates/header.html +49 -0
  25. structables-0.4.0/src/structables/templates/iframe.html +23 -0
  26. structables-0.4.0/src/structables/templates/index.html +37 -0
  27. structables-0.4.0/src/structables/templates/projects.html +49 -0
  28. structables-0.4.0/src/structables/utils/data.py +76 -0
  29. structables-0.4.0/src/structables/utils/helpers.py +551 -0
  30. structables-0.3.16/src/structables/config.py +0 -13
  31. structables-0.3.16/src/structables/main.py +0 -43
  32. structables-0.3.16/src/structables/routes/proxy.py +0 -60
  33. structables-0.3.16/src/structables/static/css/style.css +0 -282
  34. structables-0.3.16/src/structables/static/dist/css/bootstrap.min.css +0 -6
  35. structables-0.3.16/src/structables/static/dist/css/bootstrap.min.css.map +0 -1
  36. structables-0.3.16/src/structables/static/img/magnifying-glass-solid.svg +0 -1
  37. structables-0.3.16/src/structables/templates/base.html +0 -27
  38. structables-0.3.16/src/structables/templates/category.html +0 -68
  39. structables-0.3.16/src/structables/templates/footer.html +0 -21
  40. structables-0.3.16/src/structables/templates/header.html +0 -39
  41. structables-0.3.16/src/structables/templates/iframe.html +0 -12
  42. structables-0.3.16/src/structables/templates/index.html +0 -45
  43. structables-0.3.16/src/structables/templates/projects.html +0 -56
  44. structables-0.3.16/src/structables/utils/data.py +0 -56
  45. structables-0.3.16/src/structables/utils/helpers.py +0 -463
  46. {structables-0.3.16 → structables-0.4.0}/.env.example +0 -0
  47. {structables-0.3.16 → structables-0.4.0}/LICENSE +0 -0
  48. {structables-0.3.16 → structables-0.4.0}/cronjob.txt +0 -0
  49. {structables-0.3.16 → structables-0.4.0}/docker-compose-example-dev.yml +0 -0
  50. {structables-0.3.16 → structables-0.4.0}/docker-compose-example.yml +0 -0
  51. {structables-0.3.16 → structables-0.4.0}/entrypoint.sh +0 -0
  52. {structables-0.3.16 → structables-0.4.0}/src/structables/__init__.py +0 -0
  53. {structables-0.3.16 → structables-0.4.0}/src/structables/routes/__init__.py +0 -0
  54. {structables-0.3.16 → structables-0.4.0}/src/structables/static/img/logo.png +0 -0
  55. {structables-0.3.16 → structables-0.4.0}/src/structables/static/img/logo_lg.png +0 -0
  56. {structables-0.3.16 → structables-0.4.0}/src/structables/templates/400.html +0 -0
  57. {structables-0.3.16 → structables-0.4.0}/src/structables/templates/404.html +0 -0
  58. {structables-0.3.16 → structables-0.4.0}/src/structables/templates/429.html +0 -0
  59. {structables-0.3.16 → structables-0.4.0}/src/structables/templates/500.html +0 -0
  60. {structables-0.3.16 → structables-0.4.0}/src/structables/templates/archives.html +0 -0
  61. {structables-0.3.16 → structables-0.4.0}/src/structables/templates/article-review.html +0 -0
  62. {structables-0.3.16 → structables-0.4.0}/src/structables/templates/article.html +0 -0
  63. {structables-0.3.16 → structables-0.4.0}/src/structables/templates/collection.html +0 -0
  64. {structables-0.3.16 → structables-0.4.0}/src/structables/templates/contest.html +0 -0
  65. {structables-0.3.16 → structables-0.4.0}/src/structables/templates/contests.html +0 -0
  66. {structables-0.3.16 → structables-0.4.0}/src/structables/templates/member-instructables.html +0 -0
  67. {structables-0.3.16 → structables-0.4.0}/src/structables/templates/member.html +0 -0
  68. {structables-0.3.16 → structables-0.4.0}/src/structables/templates/privacypolicy.html +0 -0
  69. {structables-0.3.16 → structables-0.4.0}/src/structables/templates/sitemap.html +0 -0
  70. {structables-0.3.16 → structables-0.4.0}/src/structables/templates/style.html +0 -0
@@ -9,7 +9,7 @@ jobs:
9
9
  docker:
10
10
  name: Docker Build and Push to Docker Hub
11
11
  container:
12
- image: node:20-bookworm
12
+ image: node:22-bookworm
13
13
  steps:
14
14
  - name: Install dependencies
15
15
  run: |
@@ -25,7 +25,7 @@ jobs:
25
25
  username: ${{ secrets.DOCKERHUB_USERNAME }}
26
26
  password: ${{ secrets.DOCKERHUB_TOKEN }}
27
27
  - name: Build and push to Docker Hub
28
- uses: docker/build-push-action@v5
28
+ uses: docker/build-push-action@v6
29
29
  with:
30
30
  push: true
31
31
  tags: |
@@ -10,7 +10,7 @@ jobs:
10
10
  publish:
11
11
  name: Publish to PyPI
12
12
  container:
13
- image: node:20-bookworm
13
+ image: node:22-bookworm
14
14
 
15
15
  steps:
16
16
  - name: Check out code
@@ -6,6 +6,7 @@ privacy.md
6
6
  privacy.txt
7
7
  *.pyc
8
8
  venv/
9
+ /output
9
10
 
10
11
  # Special rules needed for building the Docker image
11
12
  /dist/*
@@ -1,4 +1,4 @@
1
- FROM alpine:3.20
1
+ FROM alpine:3.21
2
2
 
3
3
  ENV APP_ENV=/opt/venv
4
4
  ENV PATH="${APP_ENV}/bin:$PATH"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structables
3
- Version: 0.3.16
3
+ Version: 0.4.0
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
@@ -692,18 +692,26 @@ An open source alternative front-end to Instructables. This is a fork of <a href
692
692
 
693
693
  ## Instances
694
694
 
695
- | URL | Provided by | Country | Comments |
696
- | ----------------------------------------------------------------------- | ----------------------------------------------- | ------- | -------- |
697
- | [structables.private.coffee](https://structables.private.coffee/) | [Private.coffee](https://private.coffee/) | Austria | |
698
- | [structables.bloat.cat](https://structables.bloat.cat/) | [Bloat.cat](https://bloat.cat) | Germany | |
699
- | [structables.darkness.services](https://structables.darkness.services/) | [Darkness.services](https://darkness.services/) | USA | |
695
+ <!-- START_INSTANCE_LIST type:eq=clearnet -->
696
+
697
+ | URL | Provided by | Country | Notes |
698
+ | ---------------------------------------------------------------------- | ---------------------------------------------- | ---------------- | ------------- |
699
+ | [structables.private.coffee](https://structables.private.coffee) | [Private.coffee](https://private.coffee) | Austria 🇦🇹 🇪🇺 | Main instance |
700
+ | [structables.bloat.cat](https://structables.bloat.cat) | [Bloat.cat](https://bloat.cat) | Germany 🇩🇪 🇪🇺 | |
701
+ | [structables.darkness.services](https://structables.darkness.services) | [Darkness.services](https://darkness.services) | United States 🇺🇸 | |
702
+
703
+ <!-- END_INSTANCE_LIST -->
700
704
 
701
705
  ### Tor Hidden Services
702
706
 
703
- | URL | Provided by | Country | Comments |
704
- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------- | -------- |
705
- | [structables.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion](http://structables.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion/) | [Private.coffee](https://private.coffee/) | Austria | |
706
- | [structables.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion](http://structables.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion/) | [Darkness.services](https://darkness.services/) | USA | |
707
+ <!-- START_INSTANCE_LIST type:eq=onion -->
708
+
709
+ | URL | Provided by | Country | Notes |
710
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ---------------- | ------------- |
711
+ | [structables.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion](http://structables.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion) | [Private.coffee](https://private.coffee) | Austria 🇦🇹 🇪🇺 | Main instance |
712
+ | [structables.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion](http://structables.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion) | [Darkness.services](https://darkness.services) | United States 🇺🇸 | |
713
+
714
+ <!-- END_INSTANCE_LIST -->
707
715
 
708
716
  ### Adding Your Instance
709
717
 
@@ -760,6 +768,12 @@ Structables supports the use of the following environment variables for configur
760
768
  - `STRUCTABLES_UNSAFE`: If set, allow embedding untrusted iframes (if unset, display a warning and allow loading the content manually)
761
769
  - `STRUCTABLES_PRIVACY_FILE`: The path to a text file or Markdown file (with .md suffix) to use for the Privacy Policy page (if unset, try `privacy.txt` or `privacy.md` in the working directory, or fall back to a generic message)
762
770
  - `STRUCTABLES_DEBUG`: If set, log additional debug information to stdout
771
+ - `STRUCTABLES_THEME`: Allows selecting a theme for the frontend. Currently, only `dark` and `light` are supported. If not set, it will be automatically detected based on the user's system settings, and a toggle will be provided in the header.
772
+ - `STRUCTABLES_CACHE_ENABLED`: Whether to enable caching of proxied content (default: true). Set to "false" or "0" to disable caching.
773
+ - `STRUCTABLES_CACHE_DIR`: The directory to use for caching proxied content (default: `structables_cache` within the temporary directory as returned by `tempfile.gettempdir()`)
774
+ - `STRUCTABLES_CACHE_MAX_AGE`: The maximum age of cached content in seconds before it's considered stale (default: 604800 seconds, or 1 week)
775
+ - `STRUCTABLES_CACHE_MAX_SIZE`: The maximum size of the cache directory in bytes (default: 1073741824 bytes, or 1GB)
776
+ - `STRUCTABLES_CACHE_CLEANUP_INTERVAL`: How often to run the cache cleanup process in seconds (default: 3600 seconds, or 1 hour)
763
777
 
764
778
  ## License
765
779
 
@@ -13,18 +13,26 @@ An open source alternative front-end to Instructables. This is a fork of <a href
13
13
 
14
14
  ## Instances
15
15
 
16
- | URL | Provided by | Country | Comments |
17
- | ----------------------------------------------------------------------- | ----------------------------------------------- | ------- | -------- |
18
- | [structables.private.coffee](https://structables.private.coffee/) | [Private.coffee](https://private.coffee/) | Austria | |
19
- | [structables.bloat.cat](https://structables.bloat.cat/) | [Bloat.cat](https://bloat.cat) | Germany | |
20
- | [structables.darkness.services](https://structables.darkness.services/) | [Darkness.services](https://darkness.services/) | USA | |
16
+ <!-- START_INSTANCE_LIST type:eq=clearnet -->
17
+
18
+ | URL | Provided by | Country | Notes |
19
+ | ---------------------------------------------------------------------- | ---------------------------------------------- | ---------------- | ------------- |
20
+ | [structables.private.coffee](https://structables.private.coffee) | [Private.coffee](https://private.coffee) | Austria 🇦🇹 🇪🇺 | Main instance |
21
+ | [structables.bloat.cat](https://structables.bloat.cat) | [Bloat.cat](https://bloat.cat) | Germany 🇩🇪 🇪🇺 | |
22
+ | [structables.darkness.services](https://structables.darkness.services) | [Darkness.services](https://darkness.services) | United States 🇺🇸 | |
23
+
24
+ <!-- END_INSTANCE_LIST -->
21
25
 
22
26
  ### Tor Hidden Services
23
27
 
24
- | URL | Provided by | Country | Comments |
25
- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------- | -------- |
26
- | [structables.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion](http://structables.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion/) | [Private.coffee](https://private.coffee/) | Austria | |
27
- | [structables.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion](http://structables.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion/) | [Darkness.services](https://darkness.services/) | USA | |
28
+ <!-- START_INSTANCE_LIST type:eq=onion -->
29
+
30
+ | URL | Provided by | Country | Notes |
31
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ---------------- | ------------- |
32
+ | [structables.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion](http://structables.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion) | [Private.coffee](https://private.coffee) | Austria 🇦🇹 🇪🇺 | Main instance |
33
+ | [structables.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion](http://structables.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion) | [Darkness.services](https://darkness.services) | United States 🇺🇸 | |
34
+
35
+ <!-- END_INSTANCE_LIST -->
28
36
 
29
37
  ### Adding Your Instance
30
38
 
@@ -81,6 +89,12 @@ Structables supports the use of the following environment variables for configur
81
89
  - `STRUCTABLES_UNSAFE`: If set, allow embedding untrusted iframes (if unset, display a warning and allow loading the content manually)
82
90
  - `STRUCTABLES_PRIVACY_FILE`: The path to a text file or Markdown file (with .md suffix) to use for the Privacy Policy page (if unset, try `privacy.txt` or `privacy.md` in the working directory, or fall back to a generic message)
83
91
  - `STRUCTABLES_DEBUG`: If set, log additional debug information to stdout
92
+ - `STRUCTABLES_THEME`: Allows selecting a theme for the frontend. Currently, only `dark` and `light` are supported. If not set, it will be automatically detected based on the user's system settings, and a toggle will be provided in the header.
93
+ - `STRUCTABLES_CACHE_ENABLED`: Whether to enable caching of proxied content (default: true). Set to "false" or "0" to disable caching.
94
+ - `STRUCTABLES_CACHE_DIR`: The directory to use for caching proxied content (default: `structables_cache` within the temporary directory as returned by `tempfile.gettempdir()`)
95
+ - `STRUCTABLES_CACHE_MAX_AGE`: The maximum age of cached content in seconds before it's considered stale (default: 604800 seconds, or 1 week)
96
+ - `STRUCTABLES_CACHE_MAX_SIZE`: The maximum size of the cache directory in bytes (default: 1073741824 bytes, or 1GB)
97
+ - `STRUCTABLES_CACHE_CLEANUP_INTERVAL`: How often to run the cache cleanup process in seconds (default: 3600 seconds, or 1 hour)
84
98
 
85
99
  ## License
86
100
 
@@ -0,0 +1,54 @@
1
+ [
2
+ {
3
+ "name": "structables.private.coffee",
4
+ "url": "https://structables.private.coffee",
5
+ "type": "clearnet",
6
+ "provider": {
7
+ "name": "Private.coffee",
8
+ "url": "https://private.coffee"
9
+ },
10
+ "location": "AT",
11
+ "notes": "Main instance"
12
+ },
13
+ {
14
+ "name": "structables.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion",
15
+ "url": "http://structables.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion",
16
+ "type": "onion",
17
+ "provider": {
18
+ "name": "Private.coffee",
19
+ "url": "https://private.coffee"
20
+ },
21
+ "location": "AT",
22
+ "notes": "Main instance"
23
+ },
24
+ {
25
+ "name": "structables.bloat.cat",
26
+ "url": "https://structables.bloat.cat",
27
+ "type": "clearnet",
28
+ "provider": {
29
+ "name": "Bloat.cat",
30
+ "url": "https://bloat.cat"
31
+ },
32
+ "location": "DE"
33
+ },
34
+ {
35
+ "name": "structables.darkness.services",
36
+ "url": "https://structables.darkness.services",
37
+ "type": "clearnet",
38
+ "provider": {
39
+ "name": "Darkness.services",
40
+ "url": "https://darkness.services"
41
+ },
42
+ "location": "US"
43
+ },
44
+ {
45
+ "name": "structables.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion",
46
+ "url": "http://structables.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion",
47
+ "type": "onion",
48
+ "provider": {
49
+ "name": "Darkness.services",
50
+ "url": "https://darkness.services"
51
+ },
52
+ "location": "US"
53
+ }
54
+ ]
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "structables"
7
- version = "0.3.16"
7
+ version = "0.4.0"
8
8
  authors = [
9
9
  { name="Private.coffee Team", email="support@private.coffee" },
10
10
  ]
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": [
4
+ "config:recommended"
5
+ ]
6
+ }
@@ -0,0 +1,6 @@
1
+ ruff
2
+ black
3
+ isort
4
+ mypy
5
+ types-beautifulsoup4
6
+ types-colorama
@@ -0,0 +1,44 @@
1
+ import os
2
+ import tempfile
3
+
4
+ from .utils.helpers import get_typesense_api_key
5
+
6
+
7
+ class Config:
8
+ DEBUG = os.environ.get("FLASK_DEBUG", os.environ.get("STRUCTABLES_DEBUG", False))
9
+ PORT = int(os.environ.get("STRUCTABLES_PORT", 8002))
10
+ LISTEN_HOST = os.environ.get("STRUCTABLES_LISTEN_HOST", "127.0.0.1")
11
+ INVIDIOUS = os.environ.get("STRUCTABLES_INVIDIOUS")
12
+ UNSAFE = os.environ.get("STRUCTABLES_UNSAFE", False)
13
+ PRIVACY_FILE = os.environ.get("STRUCTABLES_PRIVACY_FILE")
14
+ THEME = os.environ.get("STRUCTABLES_THEME", "auto")
15
+ TYPESENSE_API_KEY = get_typesense_api_key()
16
+
17
+ # Cache settings
18
+ CACHE_ENABLED = os.environ.get("STRUCTABLES_CACHE_ENABLED", "true").lower() not in (
19
+ "false",
20
+ "0",
21
+ "no",
22
+ "off",
23
+ "n",
24
+ )
25
+ CACHE_DIR = os.environ.get("STRUCTABLES_CACHE_DIR")
26
+
27
+ if CACHE_DIR is None:
28
+ CACHE_DIR = os.path.join(
29
+ tempfile.gettempdir(), "structables_cache"
30
+ )
31
+
32
+ CACHE_MAX_AGE = int(
33
+ os.environ.get("STRUCTABLES_CACHE_MAX_AGE", 60 * 60 * 24 * 7)
34
+ ) # 1 week default
35
+ CACHE_MAX_SIZE = int(
36
+ os.environ.get("STRUCTABLES_CACHE_MAX_SIZE", 1024 * 1024 * 1024)
37
+ ) # 1GB default
38
+ CACHE_CLEANUP_INTERVAL = int(
39
+ os.environ.get("STRUCTABLES_CACHE_CLEANUP_INTERVAL", 60 * 60)
40
+ ) # 1 hour default
41
+
42
+ @staticmethod
43
+ def init_app(app):
44
+ pass
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env python
2
+
3
+ from flask import Flask
4
+ import threading
5
+ import time
6
+ import logging
7
+
8
+ from .config import Config
9
+ from .routes import init_routes
10
+ from .utils.data import update_data
11
+ from .routes.proxy import start_cache_cleanup_thread
12
+
13
+ # Configure logging
14
+ logger = logging.getLogger(__name__)
15
+
16
+ app = Flask(__name__, template_folder="templates", static_folder="static")
17
+ app.config.from_object(Config)
18
+
19
+ logger.debug("Initializing routes")
20
+ init_routes(app)
21
+ logger.debug("Performing initial data update")
22
+ update_data(app)
23
+
24
+ def background_update_data(app):
25
+ """Runs the update_data function every 5 minutes.
26
+
27
+ This replaces the need for a cron job to update the data.
28
+
29
+ Args:
30
+ app (Flask): The Flask app instance.
31
+ """
32
+ logger.debug("Starting background update thread")
33
+ while True:
34
+ logger.debug("Running scheduled data update")
35
+ update_data(app)
36
+ logger.debug("Data update complete, sleeping for 5 minutes")
37
+ time.sleep(300)
38
+
39
+ def main():
40
+ if app.config["DEBUG"]:
41
+ logging.basicConfig(
42
+ level=logging.DEBUG,
43
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
44
+ )
45
+ else:
46
+ logging.basicConfig(
47
+ level=logging.INFO,
48
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
49
+ )
50
+
51
+ logger.debug("Starting background update thread")
52
+ threading.Thread(target=background_update_data, args=(app,), daemon=True).start()
53
+
54
+ # Start the cache cleanup thread
55
+ start_cache_cleanup_thread(app)
56
+
57
+ logger.info(f"Starting Structables on {app.config['LISTEN_HOST']}:{app.config['PORT']}")
58
+ app.run(
59
+ port=app.config["PORT"],
60
+ host=app.config["LISTEN_HOST"],
61
+ debug=app.config["DEBUG"],
62
+ )
63
+
64
+ if __name__ == "__main__":
65
+ main()
@@ -1,15 +1,22 @@
1
1
  from flask import redirect
2
2
  from werkzeug.exceptions import NotFound
3
3
  from ..utils.helpers import project_list, category_page
4
+ import logging
5
+
6
+ logger = logging.getLogger(__name__)
4
7
 
5
8
 
6
9
  def init_category_routes(app):
7
10
  @app.route("/<category>/<channel>/projects/")
8
11
  def route_channel_projects(category, channel):
12
+ logger.debug(f"Rendering channel projects for {category}/{channel}")
9
13
  return project_list(app, channel.title())
10
14
 
11
15
  @app.route("/<category>/<channel>/projects/<sort>/")
12
16
  def route_channel_projects_sort(category, channel, sort):
17
+ logger.debug(
18
+ f"Rendering channel projects for {category}/{channel} sorted by {sort}"
19
+ )
13
20
  return project_list(
14
21
  app,
15
22
  channel.title(),
@@ -18,50 +25,62 @@ def init_category_routes(app):
18
25
 
19
26
  @app.route("/<category>/projects/")
20
27
  def route_category_projects(category):
28
+ logger.debug(f"Rendering category projects for {category}")
21
29
  return project_list(app, category.title())
22
30
 
23
31
  @app.route("/<category>/projects/<sort>/")
24
32
  def route_category_projects_sort(category, sort):
33
+ logger.debug(f"Rendering category projects for {category} sorted by {sort}")
25
34
  return project_list(app, category.title(), " Sorted by " + sort.title())
26
35
 
27
36
  @app.route("/projects/")
28
37
  def route_projects():
38
+ logger.debug("Rendering all projects")
29
39
  return project_list(app, "")
30
40
 
31
41
  @app.route("/projects/<sort>/")
32
42
  def route_projects_sort(sort):
43
+ logger.debug(f"Rendering all projects sorted by {sort}")
33
44
  return project_list(app, "", " Sorted by " + sort.title())
34
45
 
35
46
  @app.route("/circuits/")
36
47
  def route_circuits():
48
+ logger.debug("Rendering circuits category page")
37
49
  return category_page(app, "Circuits")
38
50
 
39
51
  @app.route("/workshop/")
40
52
  def route_workshop():
53
+ logger.debug("Rendering workshop category page")
41
54
  return category_page(app, "Workshop")
42
55
 
43
56
  @app.route("/craft/")
44
57
  def route_craft():
58
+ logger.debug("Rendering craft category page")
45
59
  return category_page(app, "Craft")
46
60
 
47
61
  @app.route("/cooking/")
48
62
  def route_cooking():
63
+ logger.debug("Rendering cooking category page")
49
64
  return category_page(app, "Cooking")
50
65
 
51
66
  @app.route("/living/")
52
67
  def route_living():
68
+ logger.debug("Rendering living category page")
53
69
  return category_page(app, "Living")
54
70
 
55
71
  @app.route("/outside/")
56
72
  def route_outside():
73
+ logger.debug("Rendering outside category page")
57
74
  return category_page(app, "Outside")
58
75
 
59
76
  @app.route("/teachers/")
60
77
  def route_teachers():
78
+ logger.debug("Rendering teachers category page")
61
79
  return category_page(app, "Teachers", True)
62
80
 
63
81
  @app.route("/<category>/<channel>/")
64
82
  def route_channel_redirect(category, channel):
83
+ logger.debug(f"Channel redirect for {category}/{channel}")
65
84
  if (
66
85
  category == "circuits"
67
86
  or category == "workshop"
@@ -71,6 +90,8 @@ def init_category_routes(app):
71
90
  or category == "outside"
72
91
  or category == "teachers"
73
92
  ):
93
+ logger.debug(f"Redirecting to /{category}/{channel}/projects/")
74
94
  return redirect(f"/{category}/{channel}/projects/", 307)
75
95
  else:
96
+ logger.warning(f"Invalid category: {category}")
76
97
  raise NotFound()
@@ -4,6 +4,9 @@ from urllib.error import HTTPError
4
4
  from ..utils.helpers import proxy
5
5
  from bs4 import BeautifulSoup
6
6
  import json
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
7
10
 
8
11
 
9
12
  def init_contest_routes(app):
@@ -14,18 +17,27 @@ def init_contest_routes(app):
14
17
  page = request.args.get("page", default=1, type=int)
15
18
  offset = (page - 1) * limit
16
19
 
20
+ logger.debug(f"Fetching contest archive page {page} with limit {limit}")
21
+
17
22
  try:
18
23
  # Fetch data using urlopen
19
24
  url = f"https://www.instructables.com/json-api/getClosedContests?limit={limit}&offset={offset}"
25
+ logger.debug(f"Making request to {url}")
20
26
  response = urlopen(url)
21
27
  data = json.loads(response.read().decode())
28
+ logger.debug(
29
+ f"Received contest archive data with {len(data.get('contests', []))} contests"
30
+ )
22
31
  except HTTPError as e:
32
+ logger.error(f"HTTP error fetching contest archive: {e.code}")
23
33
  abort(e.code)
24
34
  except Exception as e:
35
+ logger.error(f"Error fetching contest archive: {str(e)}")
25
36
  abort(500) # Handle other exceptions like JSON decode errors
26
37
 
27
38
  contests = data.get("contests", [])
28
39
  full_list_size = data.get("fullListSize", 0)
40
+ logger.debug(f"Total contests in archive: {full_list_size}")
29
41
 
30
42
  contest_list = []
31
43
  for contest in contests:
@@ -42,6 +54,7 @@ def init_contest_routes(app):
42
54
 
43
55
  # Calculate total pages
44
56
  total_pages = (full_list_size + limit - 1) // limit
57
+ logger.debug(f"Pagination: page {page}/{total_pages}")
45
58
 
46
59
  # Create pagination
47
60
  pagination = {
@@ -61,21 +74,27 @@ def init_contest_routes(app):
61
74
  )
62
75
 
63
76
  def get_entries(contest):
64
- base_url = f"https://www.instructables.com/api_proxy/search/collections/projects/documents/search"
65
- headers = {"x-typesense-api-key": app.typesense_api_key}
77
+ base_url = "https://www.instructables.com/api_proxy/search/collections/projects/documents/search"
78
+ headers = {"x-typesense-api-key": app.config["TYPESENSE_API_KEY"]}
66
79
  page, per_page = 1, 100
67
80
  all_entries = []
68
81
 
82
+ logger.debug(f"Fetching entries for contest: {contest}")
83
+
69
84
  while True:
70
85
  try:
71
86
  url = f"{base_url}?q=*&filter_by=contestPath:{contest}&sort_by=contestEntryDate:desc&per_page={per_page}&page={page}"
87
+ logger.debug(f"Making request to {url} (page {page})")
72
88
  request = Request(url, headers=headers)
73
89
  response = urlopen(request)
74
90
  data = json.loads(response.read().decode())
75
91
  except HTTPError as e:
92
+ logger.error(f"HTTP error fetching contest entries: {e.code}")
76
93
  abort(e.code)
77
94
 
78
95
  hits = data.get("hits", [])
96
+ logger.debug(f"Received {len(hits)} entries on page {page}")
97
+
79
98
  if not hits:
80
99
  break
81
100
 
@@ -84,10 +103,13 @@ def init_contest_routes(app):
84
103
  break
85
104
  page += 1
86
105
 
106
+ logger.debug(f"Total entries fetched: {len(all_entries)}")
87
107
  return all_entries
88
108
 
89
109
  @app.route("/contest/<contest>/")
90
110
  def route_contest(contest):
111
+ logger.debug(f"Fetching contest page for: {contest}")
112
+
91
113
  try:
92
114
  data = urlopen(f"https://www.instructables.com/contest/{contest}/")
93
115
  html = data.read().decode()
@@ -95,13 +117,19 @@ def init_contest_routes(app):
95
117
 
96
118
  title_tag = soup.find("h1")
97
119
  title = title_tag.get_text() if title_tag else "Contest"
120
+ logger.debug(f"Contest title: {title}")
98
121
 
99
122
  img_tag = soup.find("img", alt=lambda x: x and "Banner" in x)
100
123
  img = img_tag.get("src") if img_tag else "default.jpg"
101
124
 
102
- entry_count = len(get_entries(contest))
125
+ logger.debug(f"Fetching entries for contest: {contest}")
126
+ entries = get_entries(contest)
127
+ entry_count = len(entries)
128
+ logger.debug(f"Found {entry_count} entries")
129
+
103
130
  prizes_items = soup.select("article")
104
131
  prizes = len(prizes_items) if prizes_items else 0
132
+ logger.debug(f"Found {prizes} prizes")
105
133
 
106
134
  overview_section = soup.find("section", id="overview")
107
135
  info = (
@@ -111,10 +139,10 @@ def init_contest_routes(app):
111
139
  )
112
140
 
113
141
  except HTTPError as e:
142
+ logger.error(f"HTTP error fetching contest page: {e.code}")
114
143
  abort(e.code)
115
144
 
116
145
  entry_list = []
117
- entries = get_entries(contest)
118
146
  for entry in entries:
119
147
  doc = entry["document"]
120
148
  entry_details = {
@@ -141,18 +169,25 @@ def init_contest_routes(app):
141
169
 
142
170
  @app.route("/contest/")
143
171
  def route_contests():
172
+ logger.debug("Fetching current contests")
173
+
144
174
  try:
145
175
  # Fetch current contests from the JSON API
146
176
  response = urlopen(
147
177
  "https://www.instructables.com/json-api/getCurrentContests?limit=50&offset=0"
148
178
  )
149
179
  data = json.loads(response.read().decode())
180
+ logger.debug("Received current contests data")
150
181
  except HTTPError as e:
182
+ logger.error(f"HTTP error fetching current contests: {e.code}")
151
183
  abort(e.code)
152
184
  except Exception as e:
185
+ logger.error(f"Error fetching current contests: {str(e)}")
153
186
  abort(500) # Handle other exceptions such as JSON decode errors
154
187
 
155
188
  contests = data.get("contests", [])
189
+ logger.debug(f"Found {len(contests)} current contests")
190
+
156
191
  contest_list = []
157
192
  for contest in contests:
158
193
  contest_details = {