flavortui 1.0.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.
- flavortui-1.0.0/.gitignore +12 -0
- flavortui-1.0.0/.pylintrc +10 -0
- flavortui-1.0.0/LICENSE.txt +18 -0
- flavortui-1.0.0/PKG-INFO +77 -0
- flavortui-1.0.0/README.md +51 -0
- flavortui-1.0.0/TODO.md +21 -0
- flavortui-1.0.0/pyproject.toml +69 -0
- flavortui-1.0.0/requirements.txt +4 -0
- flavortui-1.0.0/screenshots/kitchen.png +0 -0
- flavortui-1.0.0/screenshots/with_sidebar.png +0 -0
- flavortui-1.0.0/src/flavortui/__about__.py +4 -0
- flavortui-1.0.0/src/flavortui/__init__.py +3 -0
- flavortui-1.0.0/src/flavortui/__main__.py +4 -0
- flavortui-1.0.0/src/flavortui/api/__init__.py +0 -0
- flavortui-1.0.0/src/flavortui/api/api.py +75 -0
- flavortui-1.0.0/src/flavortui/api/api_key.py +16 -0
- flavortui-1.0.0/src/flavortui/api/client.py +168 -0
- flavortui-1.0.0/src/flavortui/api/functions.py +14 -0
- flavortui-1.0.0/src/flavortui/api/settings.py +24 -0
- flavortui-1.0.0/src/flavortui/components/__init__.py +0 -0
- flavortui-1.0.0/src/flavortui/components/api_key_input.py +84 -0
- flavortui-1.0.0/src/flavortui/components/image_wrapper.py +85 -0
- flavortui-1.0.0/src/flavortui/components/popup_modal.py +52 -0
- flavortui-1.0.0/src/flavortui/components/sidebar.py +64 -0
- flavortui-1.0.0/src/flavortui/main.py +122 -0
- flavortui-1.0.0/src/flavortui/views/__init__.py +0 -0
- flavortui-1.0.0/src/flavortui/views/explore.py +488 -0
- flavortui-1.0.0/src/flavortui/views/kitchen.py +135 -0
- flavortui-1.0.0/src/flavortui/views/projects.py +365 -0
- flavortui-1.0.0/src/flavortui/views/settings.py +128 -0
- flavortui-1.0.0/src/flavortui/views/shop.py +480 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present BookCatKid <99609593+BookCatKid@users.noreply.github.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
12
|
+
portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
15
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
16
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
18
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
flavortui-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flavortui
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Feature-rich terminal UI for Flavortown: devlogs, projects, shop, and more.
|
|
5
|
+
Project-URL: Documentation, https://github.com/BookCatKid/flavortui#readme
|
|
6
|
+
Project-URL: Issues, https://github.com/BookCatKid/flavortui/issues
|
|
7
|
+
Project-URL: Source, https://github.com/BookCatKid/flavortui
|
|
8
|
+
Author-email: BookCatKid <99609593+BookCatKid@users.noreply.github.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE.txt
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
18
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: keyring
|
|
21
|
+
Requires-Dist: platformdirs
|
|
22
|
+
Requires-Dist: requests
|
|
23
|
+
Requires-Dist: textual
|
|
24
|
+
Requires-Dist: textual-image[textual]
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# FlavorTUI
|
|
28
|
+
|
|
29
|
+
[](https://pypi.org/project/flavortui)
|
|
30
|
+
[](https://pypi.org/project/flavortui)
|
|
31
|
+
|
|
32
|
+
FlavorTUI is a feature-rich terminal user interface (TUI) for Flavortown. With FlavorTUI, you can browse and create devlogs, manage your projects, explore the shop, and access other Flavortown features, all from an interactive terminal interface.
|
|
33
|
+
|
|
34
|
+
It is built using the `textual` library, which provides (imo) a great terminal UI experience. The TUI is of course written in Python 🥰. This is my first time creating a TUI so I hope its good :) Depending on your terminal, the ui might look different. It all depends on how well your terminal supports different things.
|
|
35
|
+
|
|
36
|
+
Your API key is stored "securely" using the `keyring` library, so you don't have to worry about it being exposed in your terminal history or config files.
|
|
37
|
+
|
|
38
|
+
Additionaly, your settings are store using the `platformdirs` `user_config_dir` function. Your settings are probobly stored in `~/Library/Application Support/flavortui` on macOS, `C:\Users\<user>\AppData\Local\flavortui` on Windows (`%localappdata%`), and ~/.config/flavortui (or $XDG_CONFIG_HOME) on Linux.
|
|
39
|
+
|
|
40
|
+
<div>
|
|
41
|
+
<img src="screenshots/kitchen.png" alt="Kitchen Screenshot" style="width:49%;">
|
|
42
|
+
<img src="screenshots/with_sidebar.png" alt="Shop Screenshot" style="width:49%;">
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install flavortui
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Local Development
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
python -m venv venv
|
|
55
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
56
|
+
pip install -e .
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Run with either:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
flavortui
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
or:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python -m flavortui
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## API
|
|
72
|
+
|
|
73
|
+
Flavortown API docs can be found [here](https://flavortown.hackclub.com/api/v1/docs).
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
`flavortui` is distributed under the terms of the MIT license.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# FlavorTUI
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/flavortui)
|
|
4
|
+
[](https://pypi.org/project/flavortui)
|
|
5
|
+
|
|
6
|
+
FlavorTUI is a feature-rich terminal user interface (TUI) for Flavortown. With FlavorTUI, you can browse and create devlogs, manage your projects, explore the shop, and access other Flavortown features, all from an interactive terminal interface.
|
|
7
|
+
|
|
8
|
+
It is built using the `textual` library, which provides (imo) a great terminal UI experience. The TUI is of course written in Python 🥰. This is my first time creating a TUI so I hope its good :) Depending on your terminal, the ui might look different. It all depends on how well your terminal supports different things.
|
|
9
|
+
|
|
10
|
+
Your API key is stored "securely" using the `keyring` library, so you don't have to worry about it being exposed in your terminal history or config files.
|
|
11
|
+
|
|
12
|
+
Additionaly, your settings are store using the `platformdirs` `user_config_dir` function. Your settings are probobly stored in `~/Library/Application Support/flavortui` on macOS, `C:\Users\<user>\AppData\Local\flavortui` on Windows (`%localappdata%`), and ~/.config/flavortui (or $XDG_CONFIG_HOME) on Linux.
|
|
13
|
+
|
|
14
|
+
<div>
|
|
15
|
+
<img src="screenshots/kitchen.png" alt="Kitchen Screenshot" style="width:49%;">
|
|
16
|
+
<img src="screenshots/with_sidebar.png" alt="Shop Screenshot" style="width:49%;">
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install flavortui
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Local Development
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
python -m venv venv
|
|
29
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
30
|
+
pip install -e .
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Run with either:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
flavortui
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
or:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python -m flavortui
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## API
|
|
46
|
+
|
|
47
|
+
Flavortown API docs can be found [here](https://flavortown.hackclub.com/api/v1/docs).
|
|
48
|
+
|
|
49
|
+
## License
|
|
50
|
+
|
|
51
|
+
`flavortui` is distributed under the terms of the MIT license.
|
flavortui-1.0.0/TODO.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# TODO
|
|
2
|
+
|
|
3
|
+
- performance kinda sucks (bruh)
|
|
4
|
+
- offline mode
|
|
5
|
+
- working keyboard support (oops)
|
|
6
|
+
|
|
7
|
+
- video showing off the app
|
|
8
|
+
- polish for ship
|
|
9
|
+
|
|
10
|
+
- refresh button?
|
|
11
|
+
|
|
12
|
+
duh:
|
|
13
|
+
|
|
14
|
+
- projects page
|
|
15
|
+
- explore page
|
|
16
|
+
- settings page
|
|
17
|
+
- shop page
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
- name of project creator + picture?
|
|
21
|
+
- demo, readme, repo urls
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "flavortui"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = 'Feature-rich terminal UI for Flavortown: devlogs, projects, shop, and more.'
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
keywords = []
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "BookCatKid", email = "99609593+BookCatKid@users.noreply.github.com" },
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Programming Language :: Python",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
24
|
+
"Programming Language :: Python :: Implementation :: PyPy",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"textual",
|
|
28
|
+
"textual-image[textual]",
|
|
29
|
+
"requests",
|
|
30
|
+
"platformdirs",
|
|
31
|
+
"keyring",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
flavortui = "flavortui.main:main"
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Documentation = "https://github.com/BookCatKid/flavortui#readme"
|
|
39
|
+
Issues = "https://github.com/BookCatKid/flavortui/issues"
|
|
40
|
+
Source = "https://github.com/BookCatKid/flavortui"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.version]
|
|
43
|
+
path = "src/flavortui/__about__.py"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.envs.types]
|
|
46
|
+
extra-dependencies = [
|
|
47
|
+
"mypy>=1.0.0",
|
|
48
|
+
]
|
|
49
|
+
[tool.hatch.envs.types.scripts]
|
|
50
|
+
check = "mypy --install-types --non-interactive {args:src/flavortui tests}"
|
|
51
|
+
|
|
52
|
+
[tool.coverage.run]
|
|
53
|
+
source_pkgs = ["flavortui", "tests"]
|
|
54
|
+
branch = true
|
|
55
|
+
parallel = true
|
|
56
|
+
omit = [
|
|
57
|
+
"src/flavortui/__about__.py",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[tool.coverage.paths]
|
|
61
|
+
flavortui = ["src/flavortui", "*/flavortui/src/flavortui"]
|
|
62
|
+
tests = ["tests", "*/flavortui/tests"]
|
|
63
|
+
|
|
64
|
+
[tool.coverage.report]
|
|
65
|
+
exclude_lines = [
|
|
66
|
+
"no cov",
|
|
67
|
+
"if __name__ == .__main__.:",
|
|
68
|
+
"if TYPE_CHECKING:",
|
|
69
|
+
]
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from urllib.parse import urlencode
|
|
2
|
+
|
|
3
|
+
from flavortui.api.client import get_client
|
|
4
|
+
|
|
5
|
+
# This file is surprisingly simple!
|
|
6
|
+
|
|
7
|
+
# simple helper func
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def fetch_endpoint(endpoint, api_key):
|
|
11
|
+
return get_client(api_key).fetch_endpoint(endpoint)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# User stuff
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_user(api_key, user_id="me"):
|
|
18
|
+
status_code, response = fetch_endpoint(f"users/{user_id}", api_key)
|
|
19
|
+
return status_code, response
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_users(api_key, query="", page=1):
|
|
23
|
+
params = urlencode({"page": page, "query": query})
|
|
24
|
+
return fetch_endpoint(f"users?{params}", api_key)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Store stuff
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_store(api_key):
|
|
31
|
+
return fetch_endpoint("store", api_key)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# don't think I'll ever use this but ok?
|
|
35
|
+
def get_store_item(api_key, item_id):
|
|
36
|
+
return fetch_endpoint(f"store/{item_id}", api_key)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Projects
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_project(api_key, project_id):
|
|
43
|
+
return fetch_endpoint(f"projects/{project_id}", api_key)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_projects_for_user(api_key, user_id="me"):
|
|
47
|
+
user = get_user(api_key, user_id)
|
|
48
|
+
projects = []
|
|
49
|
+
for project_id in user[1]["project_ids"]:
|
|
50
|
+
projects.append(get_project(api_key, project_id))
|
|
51
|
+
return projects
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_projects(api_key, page=1, query=""):
|
|
55
|
+
params = urlencode({"page": page, "query": query})
|
|
56
|
+
return fetch_endpoint(f"projects?{params}", api_key)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Devlogs
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_project_devlogs(api_key, project_id):
|
|
63
|
+
return fetch_endpoint(f"projects/{project_id}/devlogs", api_key)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_devlogs(api_key, page=1):
|
|
67
|
+
params = urlencode({"page": page})
|
|
68
|
+
return fetch_endpoint(f"devlogs?{params}", api_key)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Check api key
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def check_api_key(api_key):
|
|
75
|
+
return get_user(api_key, "me")[0] == 200
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import keyring
|
|
2
|
+
|
|
3
|
+
SERVICE = "flavortown-cli"
|
|
4
|
+
ACCOUNT = "default"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_api_key():
|
|
8
|
+
return keyring.get_password(SERVICE, ACCOUNT)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def save_api_key(key: str):
|
|
12
|
+
keyring.set_password(SERVICE, ACCOUNT, key)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def delete_api_key():
|
|
16
|
+
keyring.delete_password(SERVICE, ACCOUNT)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
import urllib.parse
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
CACHE_DIR = ".cache"
|
|
11
|
+
API_TIMEOUT = (3.05, 10)
|
|
12
|
+
IMAGE_TIMEOUT = (3.05, 20)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OfflineError(Exception):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ApiClient:
|
|
20
|
+
def __init__(self, api_key, settings=None):
|
|
21
|
+
self.api_key = api_key
|
|
22
|
+
self.settings = settings or {}
|
|
23
|
+
self.base_url = "https://flavortown.hackclub.com/api/v1"
|
|
24
|
+
self.headers = {
|
|
25
|
+
"Authorization": f"Bearer {api_key}",
|
|
26
|
+
"X-Flavortown-Ext-16596": "true",
|
|
27
|
+
}
|
|
28
|
+
self.is_offline = False
|
|
29
|
+
|
|
30
|
+
self._ensure_cache_dir()
|
|
31
|
+
|
|
32
|
+
self.rate_limits = {
|
|
33
|
+
# In seconds, how long we should wait between requests.
|
|
34
|
+
# In general we go for double the "required" time.
|
|
35
|
+
"projects": 24,
|
|
36
|
+
"store": 24,
|
|
37
|
+
"users_list": 24,
|
|
38
|
+
"projects_id": 4,
|
|
39
|
+
"store_id": 4,
|
|
40
|
+
"default": 2,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def _ensure_cache_dir(self):
|
|
44
|
+
os.makedirs(CACHE_DIR, exist_ok=True)
|
|
45
|
+
|
|
46
|
+
def _get_cache_file(self, endpoint, file_format):
|
|
47
|
+
hashed_key = hashlib.sha256(endpoint.encode()).hexdigest()
|
|
48
|
+
return os.path.join(CACHE_DIR, f"{hashed_key}.{file_format}")
|
|
49
|
+
|
|
50
|
+
def _save_to_cache(self, endpoint, response, status_code):
|
|
51
|
+
self._ensure_cache_dir()
|
|
52
|
+
file_name = self._get_cache_file(endpoint, "json")
|
|
53
|
+
with open(file_name, "w", encoding="utf-8") as file:
|
|
54
|
+
json.dump(
|
|
55
|
+
{
|
|
56
|
+
"timestamp": time.time(),
|
|
57
|
+
"data": response,
|
|
58
|
+
"status_code": status_code,
|
|
59
|
+
},
|
|
60
|
+
file,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def _load_from_cache(self, endpoint):
|
|
64
|
+
file_name = self._get_cache_file(endpoint, "json")
|
|
65
|
+
if os.path.exists(file_name):
|
|
66
|
+
with open(file_name, "r", encoding="utf-8") as f:
|
|
67
|
+
return json.load(f)
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
def _get_endpoint_rate_limit(self, endpoint):
|
|
71
|
+
parts = endpoint.split("/")
|
|
72
|
+
base = parts[0]
|
|
73
|
+
|
|
74
|
+
if base == "users":
|
|
75
|
+
if len(parts) == 1:
|
|
76
|
+
return self.rate_limits["users_list"]
|
|
77
|
+
return self.rate_limits["default"]
|
|
78
|
+
|
|
79
|
+
if base == "store":
|
|
80
|
+
if len(parts) == 1:
|
|
81
|
+
return self.rate_limits["store"]
|
|
82
|
+
return self.rate_limits["store_id"]
|
|
83
|
+
|
|
84
|
+
if base == "projects":
|
|
85
|
+
if len(parts) == 1:
|
|
86
|
+
return self.rate_limits["projects"]
|
|
87
|
+
return self.rate_limits["projects_id"]
|
|
88
|
+
|
|
89
|
+
# add other bases here later
|
|
90
|
+
|
|
91
|
+
return self.rate_limits["default"]
|
|
92
|
+
|
|
93
|
+
def _revalidate(self, endpoint):
|
|
94
|
+
try:
|
|
95
|
+
response = requests.get(
|
|
96
|
+
f"{self.base_url}/{endpoint}",
|
|
97
|
+
headers=self.headers,
|
|
98
|
+
timeout=API_TIMEOUT,
|
|
99
|
+
)
|
|
100
|
+
self._save_to_cache(endpoint, response.json(), response.status_code)
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
def fetch_endpoint(self, endpoint):
|
|
105
|
+
cached_file = self._load_from_cache(endpoint)
|
|
106
|
+
caching_strategy = self.settings.get("caching_strategy", "timed")
|
|
107
|
+
if cached_file:
|
|
108
|
+
# default caching
|
|
109
|
+
if (
|
|
110
|
+
time.time() - cached_file["timestamp"]
|
|
111
|
+
< self._get_endpoint_rate_limit(endpoint)
|
|
112
|
+
and caching_strategy == "timed"
|
|
113
|
+
):
|
|
114
|
+
return cached_file["status_code"], cached_file["data"]
|
|
115
|
+
# extended caching
|
|
116
|
+
if (
|
|
117
|
+
time.time() - cached_file["timestamp"]
|
|
118
|
+
< self._get_endpoint_rate_limit(endpoint) * 15
|
|
119
|
+
and caching_strategy == "extended"
|
|
120
|
+
):
|
|
121
|
+
return cached_file["status_code"], cached_file["data"]
|
|
122
|
+
# swr caching
|
|
123
|
+
if caching_strategy == "swr":
|
|
124
|
+
if time.time() - cached_file[
|
|
125
|
+
"timestamp"
|
|
126
|
+
] >= self._get_endpoint_rate_limit(endpoint):
|
|
127
|
+
threading.Thread(
|
|
128
|
+
target=self._revalidate, args=(endpoint,), daemon=True
|
|
129
|
+
).start()
|
|
130
|
+
return cached_file["status_code"], cached_file["data"]
|
|
131
|
+
|
|
132
|
+
url = f"{self.base_url}/{endpoint}"
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
response = requests.get(url, headers=self.headers, timeout=API_TIMEOUT)
|
|
136
|
+
except requests.ConnectionError as e:
|
|
137
|
+
self.is_offline = True
|
|
138
|
+
if cached_file:
|
|
139
|
+
return cached_file["status_code"], cached_file["data"]
|
|
140
|
+
raise OfflineError("Could not connect to the Flavortown server.") from e
|
|
141
|
+
self.is_offline = False
|
|
142
|
+
data = response.json()
|
|
143
|
+
self._save_to_cache(endpoint, data, response.status_code)
|
|
144
|
+
return response.status_code, data
|
|
145
|
+
|
|
146
|
+
def fetch_image(self, url):
|
|
147
|
+
ext = os.path.splitext(urllib.parse.urlparse(url).path)[1].lstrip(".") or "png"
|
|
148
|
+
if not os.path.exists(self._get_cache_file(url, ext)):
|
|
149
|
+
try:
|
|
150
|
+
response = requests.get(url, timeout=IMAGE_TIMEOUT)
|
|
151
|
+
response.raise_for_status()
|
|
152
|
+
except requests.ConnectionError:
|
|
153
|
+
raise OfflineError("Could not connect to the Flavortown server.")
|
|
154
|
+
with open(self._get_cache_file(url, ext), "wb") as f:
|
|
155
|
+
f.write(response.content)
|
|
156
|
+
return self._get_cache_file(url, ext)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
_GLOBAL_CLIENT = None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_client(api_key, settings=None):
|
|
163
|
+
global _GLOBAL_CLIENT
|
|
164
|
+
if not _GLOBAL_CLIENT or _GLOBAL_CLIENT.api_key != api_key:
|
|
165
|
+
_GLOBAL_CLIENT = ApiClient(api_key, settings)
|
|
166
|
+
elif settings is not None:
|
|
167
|
+
_GLOBAL_CLIENT.settings = settings
|
|
168
|
+
return _GLOBAL_CLIENT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def format_seconds(seconds):
|
|
5
|
+
try:
|
|
6
|
+
hours = seconds // 3600
|
|
7
|
+
minutes = (seconds % 3600) // 60
|
|
8
|
+
return f"{hours}h {minutes}m"
|
|
9
|
+
except Exception:
|
|
10
|
+
return "0h 0m"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_days_ago(utc_time_string):
|
|
14
|
+
return (datetime.now(timezone.utc) - datetime.fromisoformat(utc_time_string)).days
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from platformdirs import user_config_dir
|
|
5
|
+
|
|
6
|
+
SETTINGS_PATH = Path(user_config_dir("flavortui")) / "settings.json"
|
|
7
|
+
DEFAULTS = {"image_mode": "auto", "caching_strategy": "swr"}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_settings() -> dict:
|
|
11
|
+
if SETTINGS_PATH.exists():
|
|
12
|
+
try:
|
|
13
|
+
with open(SETTINGS_PATH, encoding="utf-8") as f:
|
|
14
|
+
saved = json.load(f)
|
|
15
|
+
return {**DEFAULTS, **saved}
|
|
16
|
+
except (json.JSONDecodeError, OSError):
|
|
17
|
+
pass
|
|
18
|
+
return dict(DEFAULTS)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def save_settings(settings: dict) -> None:
|
|
22
|
+
SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
with open(SETTINGS_PATH, "w", encoding="utf-8") as f:
|
|
24
|
+
json.dump(settings, f, indent=2)
|
|
File without changes
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from textual.app import ComposeResult
|
|
2
|
+
from textual.widgets import Button, Input, Static
|
|
3
|
+
|
|
4
|
+
from flavortui.api.api import check_api_key
|
|
5
|
+
from flavortui.api.api_key import delete_api_key, get_api_key, save_api_key
|
|
6
|
+
from flavortui.components.popup_modal import PopupModal
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ApiKeyInput(PopupModal):
|
|
10
|
+
DEFAULT_CSS = """
|
|
11
|
+
#dialog {
|
|
12
|
+
height: auto;
|
|
13
|
+
max-height: 90%;
|
|
14
|
+
padding: 2 4;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#dialog-content {
|
|
18
|
+
height: auto;
|
|
19
|
+
align-horizontal: center;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#title {
|
|
23
|
+
text-align: center;
|
|
24
|
+
width: 100%;
|
|
25
|
+
margin-bottom: 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#subtitle {
|
|
29
|
+
text-align: center;
|
|
30
|
+
width: 100%;
|
|
31
|
+
margin-bottom: 1;
|
|
32
|
+
}
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, callback_function, **kwargs):
|
|
36
|
+
super().__init__(**kwargs)
|
|
37
|
+
self._callback_function = callback_function
|
|
38
|
+
|
|
39
|
+
def compose_content(self) -> ComposeResult:
|
|
40
|
+
yield Static("🔒 Flavortown Api Key Required", id="title")
|
|
41
|
+
yield Static("Please enter your api key to continue.", id="subtitle")
|
|
42
|
+
yield Input(password=True, placeholder="Api Key")
|
|
43
|
+
|
|
44
|
+
def compose_footer(self) -> ComposeResult:
|
|
45
|
+
return [
|
|
46
|
+
Button("Save API Key", id="save_key", variant="success"),
|
|
47
|
+
Button("Print API Key", id="print_key", variant="primary"),
|
|
48
|
+
Button("Delete API Key", id="delete_key", variant="error"),
|
|
49
|
+
Button("Close", id="close", variant="default"),
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
def on_button_pressed(self, event: Button.Pressed):
|
|
53
|
+
if event.button.id == "save_key":
|
|
54
|
+
self._save_key()
|
|
55
|
+
elif event.button.id == "print_key":
|
|
56
|
+
key = get_api_key()
|
|
57
|
+
if key:
|
|
58
|
+
self.notify(f"API Key: {key}", timeout=5.0)
|
|
59
|
+
else:
|
|
60
|
+
self.notify("No API key saved.", timeout=3.0)
|
|
61
|
+
elif event.button.id == "delete_key":
|
|
62
|
+
try:
|
|
63
|
+
delete_api_key()
|
|
64
|
+
self.notify("API key deleted successfully.", timeout=2.0)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
self.notify(f"Error deleting API key: {e}", timeout=2.0)
|
|
67
|
+
elif event.button.id == "close":
|
|
68
|
+
self.app.pop_screen()
|
|
69
|
+
|
|
70
|
+
def _save_key(self) -> None:
|
|
71
|
+
value = self.query_one(Input).value
|
|
72
|
+
if not value.strip():
|
|
73
|
+
self.notify("API key cannot be empty.", timeout=2.0)
|
|
74
|
+
return
|
|
75
|
+
if not check_api_key(value):
|
|
76
|
+
self.notify("Invalid API key. Please check and try again.", timeout=4.0)
|
|
77
|
+
return
|
|
78
|
+
try:
|
|
79
|
+
save_api_key(value)
|
|
80
|
+
self.notify("Api key saved successfully!", timeout=2.0)
|
|
81
|
+
self.app.pop_screen()
|
|
82
|
+
self._callback_function()
|
|
83
|
+
except Exception as e:
|
|
84
|
+
self.notify(f"Error saving api key: {e}", timeout=2.0)
|