mirror of
https://github.com/Donkie/Spoolman.git
synced 2026-10-22 01:52:17 -05:00
98 files changed
+2485
-2210
No files matched your search
@@ -25,13 +25,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up PDM
|
||||
uses: pdm-project/setup-pdm@v4.1
|
||||
uses: actions/checkout@v6
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: "3.9"
|
||||
- run: pdm sync
|
||||
- run: pdm run docs
|
||||
version: "0.9.26"
|
||||
- name: "Set up Python"
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version-file: "pyproject.toml"
|
||||
- name: Install Poe the Poet
|
||||
run: uv tool install poethepoet
|
||||
- run: uv sync
|
||||
- run: poe generate-docs
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5.0.0
|
||||
- name: Upload artifact
|
||||
|
||||
+37
-23
@@ -25,18 +25,33 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
python-version: "3.9"
|
||||
node-version: '20'
|
||||
|
||||
- name: Install pre-commit
|
||||
run: pip install pre-commit==3.3.3
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: "0.9.26"
|
||||
|
||||
- name: "Set up Python"
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version-file: "pyproject.toml"
|
||||
|
||||
- name: Install lefthook
|
||||
run: uv add --dev lefthook
|
||||
|
||||
- name: Install client dependencies
|
||||
run: |
|
||||
cd client
|
||||
npm ci
|
||||
|
||||
- name: Run pre-commit
|
||||
run: pre-commit run --all-files
|
||||
run: uv run lefthook run ci
|
||||
#
|
||||
# Build the Spoolman client for baking into the docker images
|
||||
# This is done on native hardware to speed things up, vite is very slow on emulated hardware
|
||||
@@ -45,22 +60,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: "3.9"
|
||||
|
||||
- name: Export requirements
|
||||
run: |
|
||||
pip install pdm
|
||||
pdm export -o requirements.txt --without-hashes
|
||||
version: "0.9.26"
|
||||
|
||||
- name: "Set up Python"
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version-file: "pyproject.toml"
|
||||
|
||||
- name: Install client dependencies
|
||||
run: |
|
||||
@@ -114,7 +129,7 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download client
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
@@ -166,7 +181,7 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.6.1
|
||||
@@ -214,7 +229,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download built images
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
@@ -246,7 +261,7 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download client
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
@@ -287,7 +302,7 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download client
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
@@ -327,7 +342,7 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download client
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
@@ -441,4 +456,3 @@ jobs:
|
||||
files: /tmp/spoolman/spoolman.zip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
body: "⚠️ TODO ⚠️"
|
||||
|
||||
@@ -19,31 +19,32 @@ permissions:
|
||||
jobs:
|
||||
bump-version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: "3.9"
|
||||
version: "0.9.26"
|
||||
|
||||
- name: "Set up Python"
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version-file: "pyproject.toml"
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup PDM
|
||||
run: |
|
||||
pip install pdm pdm-bump
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
git config --global user.name "${GITHUB_ACTOR}"
|
||||
git config --global user.email "${GITHUB_ACTOR_ID}+${GITHUB_ACTOR}@users.noreply.github.com"
|
||||
pdm run bump ${{ github.event.inputs.bump }}
|
||||
uv run bump2version ${{ github.event.inputs.bump }}
|
||||
|
||||
- name: Push
|
||||
run: |
|
||||
|
||||
@@ -26,6 +26,7 @@ share/python-wheels/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
requirements.txt
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
default_language_version:
|
||||
python: python3.9
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- id: check-case-conflict
|
||||
- id: check-merge-conflict
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 25.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: ["--check"]
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: ["--target-version", "py39"]
|
||||
- repo: https://github.com/pdm-project/pdm
|
||||
rev: 2.22.4
|
||||
hooks:
|
||||
- id: pdm-lock-check
|
||||
+19
-16
@@ -1,4 +1,9 @@
|
||||
FROM python:3.12-slim-bookworm AS python-builder
|
||||
FROM ghcr.io/astral-sh/uv:python3.14-bookworm-slim AS python-builder
|
||||
|
||||
ENV UV_COMPILE_BYTECODE=1
|
||||
ENV UV_LINK_MODE=copy
|
||||
ENV UV_NO_DEV=1
|
||||
ENV UV_PYTHON_DOWNLOADS=0
|
||||
|
||||
# Install dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
@@ -6,33 +11,31 @@ RUN apt-get update && apt-get install -y \
|
||||
python3-dev \
|
||||
libpq-dev \
|
||||
libffi-dev \
|
||||
python3-pip \
|
||||
python3-setuptools \
|
||||
python3-wheel \
|
||||
python3-pdm \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add local user so we don't run as root
|
||||
RUN groupmod -g 1000 users \
|
||||
&& useradd -u 911 -U app \
|
||||
&& usermod -G users app
|
||||
# RUN groupmod -g 1000 users \
|
||||
# && useradd -u 911 -U app \
|
||||
# && usermod -G users app
|
||||
|
||||
ENV PATH="/home/app/.local/bin:${PATH}"
|
||||
# ENV PATH="/home/app/.local/bin:${PATH}"
|
||||
|
||||
# Copy and install dependencies
|
||||
COPY --chown=app:app pyproject.toml /home/app/spoolman/
|
||||
COPY --chown=app:app pdm.lock /home/app/spoolman/
|
||||
# Install dependencies
|
||||
WORKDIR /home/app/spoolman
|
||||
RUN pdm sync --prod --no-editable
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --locked --no-install-project
|
||||
|
||||
# Copy and install app
|
||||
COPY --chown=app:app migrations /home/app/spoolman/migrations
|
||||
COPY --chown=app:app spoolman /home/app/spoolman/spoolman
|
||||
COPY --chown=app:app alembic.ini /home/app/spoolman/
|
||||
COPY --chown=app:app README.md /home/app/spoolman/
|
||||
COPY --chown=app:app alembic.ini README.md uv.lock pyproject.toml /home/app/spoolman/
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --locked
|
||||
|
||||
FROM python:3.12-slim-bookworm AS python-runner
|
||||
FROM python:3.14-slim-bookworm AS python-runner
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/Donkie/Spoolman
|
||||
LABEL org.opencontainers.image.description="Keep track of your inventory of 3D-printer filament spools."
|
||||
|
||||
@@ -31,7 +31,7 @@ Spoolman is a self-hosted web service designed to help you efficiently manage yo
|
||||
* [Moonraker](https://moonraker.readthedocs.io/en/latest/configuration/#spoolman) and most front-ends (Fluidd, KlipperScreen, Mainsail, ...)
|
||||
* [OctoPrint](https://github.com/mdziekon/octoprint-spoolman)
|
||||
* [OctoEverywhere](https://octoeverywhere.com/spoolman?source=github_spoolman)
|
||||
* [Homeassistant](https://github.com/Disane87/spoolman-homeassistant)
|
||||
* [Home Assistant](https://github.com/Disane87/spoolman-homeassistant)
|
||||
|
||||
**Web client preview:**
|
||||

|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
/* eslint-env node */
|
||||
|
||||
module.exports = {
|
||||
env: { browser: true, es2020: true },
|
||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
|
||||
plugins: ["react-refresh"],
|
||||
rules: {
|
||||
"react-refresh/only-export-components": "warn",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
public/locales
|
||||
dist
|
||||
dev-dist
|
||||
node_modules
|
||||
.vscode
|
||||
.vite
|
||||
@@ -0,0 +1,23 @@
|
||||
import js from "@eslint/js";
|
||||
import eslintConfigPrettier from "eslint-config-prettier/flat";
|
||||
import reactPlugin from "eslint-plugin-react";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(["node_modules/", "dist/", "dev-dist/"]),
|
||||
{
|
||||
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
||||
plugins: { js, reactHooks, reactRefresh },
|
||||
extends: ["js/recommended"],
|
||||
languageOptions: { globals: globals.browser },
|
||||
settings: { react: { version: "detect" } },
|
||||
},
|
||||
tseslint.configs.recommended,
|
||||
reactPlugin.configs.flat.recommended,
|
||||
reactPlugin.configs.flat["jsx-runtime"],
|
||||
eslintConfigPrettier,
|
||||
]);
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
Generated
+542
-232
File diff suppressed because it is too large.
Load diff
+15
-10
@@ -16,16 +16,13 @@
|
||||
"@refinedev/simple-rest": "^6.0.1",
|
||||
"@tanstack/react-query": "^5.90.16",
|
||||
"@tanstack/react-query-devtools": "^5.91.2",
|
||||
"@types/loadable__component": "^5.13.10",
|
||||
"@types/lodash": "^4.17.21",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@yudiel/react-qr-scanner": "^2.5.0",
|
||||
"axios": "^1.13.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"html-to-image": "^1.11.13",
|
||||
"i18next": "^25.7.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^19.2.3",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
@@ -38,17 +35,22 @@
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@refinedev/cli": "^2.16.50",
|
||||
"@simbathesailor/use-what-changed": "^2.0.0",
|
||||
"@types/loadable__component": "^5.13.10",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
||||
"@typescript-eslint/parser": "^8.52.0",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint": "^9.39.2",
|
||||
"globals": "^17.0.0",
|
||||
"prettier": "3.7.4",
|
||||
"typescript-eslint": "^8.52.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.0",
|
||||
"vite-plugin-mkcert": "^1.17.9",
|
||||
@@ -60,7 +62,10 @@
|
||||
"build.zip": "cd ./dist && zip -r spoolman-client.zip ./ ./ && cd ..",
|
||||
"preview": "refine start",
|
||||
"refine": "refine",
|
||||
"check-i18n": "node scripts/check-i18n.js"
|
||||
"check-i18n": "node scripts/check-i18n.js",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"format": "prettier --write .",
|
||||
"format-check": "prettier --check ."
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { readdirSync, readFileSync, statSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import process from "process";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ const LoadableResourcePage = loadable(
|
||||
{
|
||||
fallback: <div>Page is Loading...</div>,
|
||||
cacheKey: (props: ResourcePageProps) => `${props.resource}-${props.page}-${props.mode ?? ""}`,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
interface LoadablePageProps {
|
||||
|
||||
@@ -69,13 +69,13 @@ interface CustomColumnProps<Obj> {
|
||||
render?: (value: any, record: Obj, index: number) => React.ReactNode;
|
||||
onCell?: (
|
||||
data: Obj,
|
||||
index?: number
|
||||
index?: number,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) => React.HTMLAttributes<any> | React.TdHTMLAttributes<any>;
|
||||
}
|
||||
|
||||
function Column<Obj extends Entity>(
|
||||
props: BaseColumnProps<Obj> & FilteredColumnProps & CustomColumnProps<Obj>
|
||||
props: BaseColumnProps<Obj> & FilteredColumnProps & CustomColumnProps<Obj>,
|
||||
): ColumnType<Obj> | undefined {
|
||||
const t = props.t;
|
||||
const navigate = props.navigate;
|
||||
@@ -100,7 +100,7 @@ function Column<Obj extends Entity>(
|
||||
columnProps.sorter = true;
|
||||
columnProps.sortOrder = getSortOrderForField(
|
||||
typeSorters<Obj>(props.tableState.sorters),
|
||||
props.dataId ?? (props.id as keyof Obj)
|
||||
props.dataId ?? (props.id as keyof Obj),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ export function SortedColumn<Obj extends Entity>(props: BaseColumnProps<Obj>) {
|
||||
}
|
||||
|
||||
export function RichColumn<Obj extends Entity>(
|
||||
props: Omit<BaseColumnProps<Obj>, "transform"> & { transform?: (value: unknown) => string }
|
||||
props: Omit<BaseColumnProps<Obj>, "transform"> & { transform?: (value: unknown) => string },
|
||||
) {
|
||||
return Column({
|
||||
...props,
|
||||
@@ -269,7 +269,7 @@ export function DateColumn<Obj extends Entity>(props: BaseColumnProps<Obj>) {
|
||||
|
||||
export function ActionsColumn<Obj extends Entity>(
|
||||
title: string,
|
||||
actionsFn: (record: Obj) => Action[]
|
||||
actionsFn: (record: Obj) => Action[],
|
||||
): ColumnType<Obj> | undefined {
|
||||
return {
|
||||
title,
|
||||
|
||||
@@ -8,7 +8,7 @@ type MethodTypesWithBody = "post" | "put" | "patch";
|
||||
|
||||
const dataProvider = (
|
||||
apiUrl: string,
|
||||
httpClient: AxiosInstance = axiosInstance
|
||||
httpClient: AxiosInstance = axiosInstance,
|
||||
): Omit<Required<DataProvider>, "createMany" | "updateMany" | "deleteMany"> => ({
|
||||
getList: async ({ resource, meta, pagination, sorters, filters }) => {
|
||||
const url = `${apiUrl}/${resource}`;
|
||||
|
||||
@@ -165,14 +165,14 @@ export function ExtraFieldFormItem(props: { field: Field; setDefaultValue?: bool
|
||||
* @returns
|
||||
*/
|
||||
export function ParsedExtras<T extends { extra?: { [key: string]: string } }>(
|
||||
obj: T
|
||||
obj: T,
|
||||
): Omit<T, "extra"> & { extra?: { [key: string]: unknown } } {
|
||||
if (obj.extra) {
|
||||
const newExtra: { [key: string]: unknown } = {};
|
||||
Object.entries(obj.extra).forEach(([key, value]) => {
|
||||
try {
|
||||
newExtra[key] = JSON.parse(value);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
newExtra[key] = value;
|
||||
}
|
||||
});
|
||||
@@ -191,7 +191,7 @@ export function ParsedExtras<T extends { extra?: { [key: string]: string } }>(
|
||||
* @returns
|
||||
*/
|
||||
export function StringifiedExtras<T extends { extra?: { [key: string]: unknown } }>(
|
||||
obj: T
|
||||
obj: T,
|
||||
): Omit<T, "extra"> & { extra?: { [key: string]: string } } {
|
||||
if (obj.extra) {
|
||||
const newExtra: { [key: string]: string } = {};
|
||||
|
||||
@@ -23,7 +23,7 @@ export function FilamentImportModal(props: {
|
||||
item.manufacturer,
|
||||
item.material,
|
||||
item.weight,
|
||||
item.spool_type
|
||||
item.spool_type,
|
||||
),
|
||||
value: item.id,
|
||||
item: item,
|
||||
|
||||
@@ -10,7 +10,7 @@ import QRCodeScannerModal from "../qrCodeScanner";
|
||||
|
||||
const { useToken } = theme;
|
||||
|
||||
export const Header: React.FC<RefineThemedLayoutHeaderProps> = ({ sticky }) => {
|
||||
export const Header = ({ sticky }: RefineThemedLayoutHeaderProps) => {
|
||||
const { token } = useToken();
|
||||
const locale = useGetLocale();
|
||||
const changeLanguage = useSetLocale();
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getBasePath } from "../utils/url";
|
||||
import { Header } from "./header";
|
||||
import { Version } from "./version";
|
||||
|
||||
const SpoolmanFooter: React.FC = () => {
|
||||
const SpoolmanFooter = () => {
|
||||
const t = useTranslate();
|
||||
|
||||
return (
|
||||
@@ -46,11 +46,7 @@ const SpoolmanFooter: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
interface SpoolmanLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SpoolmanLayout: React.FC<SpoolmanLayoutProps> = ({ children }) => (
|
||||
export const SpoolmanLayout = ({ children }: { children: React.ReactNode }) => (
|
||||
<ThemedLayout
|
||||
Header={() => <Header sticky />}
|
||||
Sider={() => (
|
||||
|
||||
@@ -59,7 +59,7 @@ function subscribeSingle(
|
||||
channel: string,
|
||||
resource: string,
|
||||
callback: (event: LiveEvent) => void,
|
||||
id?: BaseKey
|
||||
id?: BaseKey,
|
||||
) {
|
||||
// Verify that WebSockets are supported
|
||||
if (!("WebSocket" in window)) {
|
||||
|
||||
@@ -14,7 +14,7 @@ export function useLiveify<Data extends { id: number }>(
|
||||
resource: string,
|
||||
dataSource: Data[],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
transformPayload: (payload: any) => Data
|
||||
transformPayload: (payload: any) => Data,
|
||||
) {
|
||||
// New state that holds the dataSource with updated values from the live provider
|
||||
const [updatedDataSource, setUpdatedDataSource] = useState<Data[]>(dataSource);
|
||||
@@ -40,7 +40,7 @@ export function useLiveify<Data extends { id: number }>(
|
||||
setUpdatedDataSource((prev) =>
|
||||
prev.map((item) => {
|
||||
return item.id === event.payload.data.id ? transformPayload(event.payload.data) : item;
|
||||
})
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ type UndoableNotificationProps = {
|
||||
undoableTimeout: OpenNotificationParams["undoableTimeout"];
|
||||
};
|
||||
|
||||
const UndoableNotification: React.FC<UndoableNotificationProps> = ({ message, cancelMutation, undoableTimeout }) => (
|
||||
const UndoableNotification = ({ message, cancelMutation, undoableTimeout }: UndoableNotificationProps) => (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NumberFieldProps } from "@refinedev/antd/dist/components/fields/types";
|
||||
import { Typography } from "antd";
|
||||
import React from "react";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -17,7 +16,7 @@ type Props = NumberFieldProps & {
|
||||
*
|
||||
* @see {@link https://refine.dev/docs/ui-frameworks/antd/components/fields/number} for more details.
|
||||
*/
|
||||
export const NumberFieldUnit: React.FC<Props> = ({ value, locale, options, unit }) => {
|
||||
export const NumberFieldUnit = ({ value, locale, options, unit }: Props) => {
|
||||
const number = Number(value);
|
||||
|
||||
return (
|
||||
|
||||
@@ -43,7 +43,7 @@ export function useSpoolmanFilamentFilter(enabled: boolean = false) {
|
||||
height: "1.4em",
|
||||
backgroundColor: "#" + filament.color_hex,
|
||||
}}
|
||||
></div>
|
||||
></div>,
|
||||
);
|
||||
}
|
||||
if (filament.material) {
|
||||
|
||||
@@ -2,10 +2,10 @@ import { CameraOutlined } from "@ant-design/icons";
|
||||
import { useTranslate } from "@refinedev/core";
|
||||
import { IDetectedBarcode, Scanner } from "@yudiel/react-qr-scanner";
|
||||
import { FloatButton, Modal, Space } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
const QRCodeScannerModal: React.FC = () => {
|
||||
const QRCodeScannerModal = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [lastError, setLastError] = useState<string | null>(null);
|
||||
const t = useTranslate();
|
||||
|
||||
@@ -3,15 +3,15 @@ import "./spoolIcon.css";
|
||||
interface Props {
|
||||
color: string | { colors: string[]; vertical: boolean };
|
||||
size?: "small" | "large";
|
||||
no_margin? : boolean
|
||||
no_margin?: boolean;
|
||||
}
|
||||
|
||||
export default function SpoolIcon(props: Readonly<Props>) {
|
||||
let dirClass = "vertical";
|
||||
let cols = [];
|
||||
let size = props.size ? props.size : "small";
|
||||
let no_margin = props.no_margin ? "no-margin" : "";
|
||||
|
||||
const size = props.size ? props.size : "small";
|
||||
const no_margin = props.no_margin ? "no-margin" : "";
|
||||
|
||||
if (typeof props.color === "string") {
|
||||
cols = [props.color];
|
||||
} else {
|
||||
|
||||
@@ -15,7 +15,7 @@ interface IInfo {
|
||||
build_date?: string;
|
||||
}
|
||||
|
||||
export const Version: React.FC = () => {
|
||||
export const Version = () => {
|
||||
const infoResult = useQuery<IInfo>({
|
||||
queryKey: ["info"],
|
||||
queryFn: async () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ type ColorModeContextType = {
|
||||
|
||||
export const ColorModeContext = createContext<ColorModeContextType>({} as ColorModeContextType);
|
||||
|
||||
export const ColorModeContextProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
export const ColorModeContextProvider = ({ children }: PropsWithChildren) => {
|
||||
const colorModeFromLocalStorage = localStorage.getItem("colorMode");
|
||||
const isSystemPreferenceDark = window?.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
|
||||
|
||||
@@ -13,5 +13,5 @@ root.render(
|
||||
<React.Suspense fallback="loading">
|
||||
<App />
|
||||
</React.Suspense>
|
||||
</React.StrictMode>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -26,7 +26,7 @@ type IFilamentRequest = Omit<IFilamentParsedExtras, "id" | "registered"> & {
|
||||
vendor_id: number;
|
||||
};
|
||||
|
||||
export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps> = (props) => {
|
||||
export const FilamentCreate = (props: IResourceComponentsProps & CreateOrCloneProps) => {
|
||||
const t = useTranslate();
|
||||
const extraFields = useGetFields(EntityType.filament);
|
||||
const currency = useCurrency();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Edit, useForm, useSelect } from "@refinedev/antd";
|
||||
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
|
||||
import { HttpError, useTranslate } from "@refinedev/core";
|
||||
import { Alert, ColorPicker, DatePicker, Form, Input, InputNumber, message, Radio, Select, Typography } from "antd";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import dayjs from "dayjs";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
|
||||
import { MultiColorPicker } from "../../components/multiColorPicker";
|
||||
import { formatNumberOnUserInput, numberParser, numberParserAllowEmpty } from "../../utils/parsing";
|
||||
@@ -19,7 +19,7 @@ We also need to stringify them again before sending them back to the API, which
|
||||
the form's onFinish method. Form.Item's normalize should do this, but it doesn't seem to work.
|
||||
*/
|
||||
|
||||
export const FilamentEdit: React.FC<IResourceComponentsProps> = () => {
|
||||
export const FilamentEdit = () => {
|
||||
const t = useTranslate();
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [hasChanged, setHasChanged] = useState(false);
|
||||
|
||||
@@ -16,8 +16,8 @@ export async function createFilamentFromExternal(externalFilament: ExternalFilam
|
||||
if (externalFilament.color_hex) {
|
||||
color_hex = externalFilament.color_hex;
|
||||
} else if (externalFilament.color_hexes && externalFilament.color_hexes.length > 0) {
|
||||
multi_color_hexes = externalFilament.color_hexes.join(",")
|
||||
multi_color_direction = externalFilament.multi_color_direction
|
||||
multi_color_hexes = externalFilament.color_hexes.join(",");
|
||||
multi_color_direction = externalFilament.multi_color_direction;
|
||||
}
|
||||
|
||||
const body: Omit<IFilament, "id" | "registered" | "extra"> & { vendor_id: number } = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EditOutlined, EyeOutlined, FileOutlined, FilterOutlined, PlusSquareOutlined } from "@ant-design/icons";
|
||||
import { List, useTable } from "@refinedev/antd";
|
||||
import { IResourceComponentsProps, useInvalidate, useNavigation, useTranslate } from "@refinedev/core";
|
||||
import { useInvalidate, useNavigation, useTranslate } from "@refinedev/core";
|
||||
import { Button, Dropdown, Table } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
@@ -69,10 +69,10 @@ const allColumns: (keyof IFilamentCollapsed & string)[] = [
|
||||
"comment",
|
||||
];
|
||||
const defaultColumns = allColumns.filter(
|
||||
(column_id) => ["registered", "density", "diameter", "spool_weight"].indexOf(column_id) === -1
|
||||
(column_id) => ["registered", "density", "diameter", "spool_weight"].indexOf(column_id) === -1,
|
||||
);
|
||||
|
||||
export const FilamentList: React.FC<IResourceComponentsProps> = () => {
|
||||
export const FilamentList = () => {
|
||||
const t = useTranslate();
|
||||
const invalidate = useInvalidate();
|
||||
const navigate = useNavigate();
|
||||
@@ -139,7 +139,7 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
|
||||
// Collapse the dataSource to a mutable list
|
||||
const queryDataSource: IFilamentCollapsed[] = useMemo(
|
||||
() => (tableProps.dataSource || []).map((record) => ({ ...record })),
|
||||
[tableProps.dataSource]
|
||||
[tableProps.dataSource],
|
||||
);
|
||||
const dataSource = useLiveify("filament", queryDataSource, collapseFilament);
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { DateField, NumberField, Show, TextField } from "@refinedev/antd";
|
||||
import { IResourceComponentsProps, useShow, useTranslate } from "@refinedev/core";
|
||||
import { useShow, useTranslate } from "@refinedev/core";
|
||||
import { Button, Typography } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { ExtraFieldDisplay } from "../../components/extraFields";
|
||||
import { NumberFieldUnit } from "../../components/numberField";
|
||||
@@ -16,7 +15,7 @@ dayjs.extend(utc);
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
export const FilamentShow: React.FC<IResourceComponentsProps> = () => {
|
||||
export const FilamentShow = () => {
|
||||
const t = useTranslate();
|
||||
const navigate = useNavigate();
|
||||
const extraFields = useGetFields(EntityType.filament);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { FileOutlined, HighlightOutlined, UserOutlined } from "@ant-design/icons";
|
||||
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
|
||||
import { useTranslate } from "@refinedev/core";
|
||||
import { List, theme } from "antd";
|
||||
import { Content } from "antd/es/layout/layout";
|
||||
import Title from "antd/es/typography/Title";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import React from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
|
||||
@@ -13,7 +12,7 @@ dayjs.extend(utc);
|
||||
|
||||
const { useToken } = theme;
|
||||
|
||||
export const Help: React.FC<IResourceComponentsProps> = () => {
|
||||
export const Help = () => {
|
||||
const { token } = useToken();
|
||||
const t = useTranslate();
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { FileOutlined, HighlightOutlined, PlusOutlined, UnorderedListOutlined, UserOutlined } from "@ant-design/icons";
|
||||
import { IResourceComponentsProps, useList, useTranslate } from "@refinedev/core";
|
||||
import { useList, useTranslate } from "@refinedev/core";
|
||||
import { Card, Col, Row, Statistic, theme } from "antd";
|
||||
import { Content } from "antd/es/layout/layout";
|
||||
import Title from "antd/es/typography/Title";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import React, { ReactNode } from "react";
|
||||
import { ReactNode } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
import Logo from "../../icon.svg?react";
|
||||
@@ -15,7 +15,7 @@ dayjs.extend(utc);
|
||||
|
||||
const { useToken } = theme;
|
||||
|
||||
export const Home: React.FC<IResourceComponentsProps> = () => {
|
||||
export const Home = () => {
|
||||
const { token } = useToken();
|
||||
const t = useTranslate();
|
||||
|
||||
@@ -39,10 +39,10 @@ export const Home: React.FC<IResourceComponentsProps> = () => {
|
||||
<Card
|
||||
loading={props.loading}
|
||||
actions={[
|
||||
<Link to={`/${props.resource}`}>
|
||||
<Link to={`/${props.resource}`} key="resource">
|
||||
<UnorderedListOutlined />
|
||||
</Link>,
|
||||
<Link to={`/${props.resource}/create`}>
|
||||
<Link to={`/${props.resource}/create`} key="create">
|
||||
<PlusOutlined />
|
||||
</Link>,
|
||||
]}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, Input, theme } from "antd";
|
||||
import type { Identifier, XYCoord } from "dnd-core";
|
||||
import { useRef, useState } from "react";
|
||||
import { useDrag, useDrop } from "react-dnd";
|
||||
import { DragSourceMonitor, useDrag, useDrop } from "react-dnd";
|
||||
|
||||
import { DeleteOutlined } from "@ant-design/icons";
|
||||
import { useTranslate, useUpdate } from "@refinedev/core";
|
||||
@@ -9,7 +9,6 @@ import { ISpool } from "../../spools/model";
|
||||
import { DragItem, ItemTypes, SpoolDragItem } from "../dnd";
|
||||
import { EMPTYLOC } from "../functions";
|
||||
import { SpoolList } from "./spoolList";
|
||||
import { useGetSetting } from "../../../utils/querySettings";
|
||||
|
||||
const { useToken } = theme;
|
||||
|
||||
@@ -125,7 +124,7 @@ export function Location({
|
||||
item: () => {
|
||||
return { title, index };
|
||||
},
|
||||
collect: (monitor: any) => ({
|
||||
collect: (monitor: DragSourceMonitor<{ title: string; index: number }>) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ export function LocationContainer() {
|
||||
// Create list of locations that's sorted
|
||||
const locationsList = useMemo(() => {
|
||||
// Start with the default loc
|
||||
let allLocs = [];
|
||||
const allLocs = [];
|
||||
if (EMPTYLOC in spoolLocations) {
|
||||
allLocs.push(EMPTYLOC);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useNavigation, useTranslate, useUpdate } from "@refinedev/core";
|
||||
import { Button, theme } from "antd";
|
||||
import type { Identifier, XYCoord } from "dnd-core";
|
||||
import { useDrag, useDrop } from "react-dnd";
|
||||
import { DragSourceMonitor, useDrag, useDrop } from "react-dnd";
|
||||
|
||||
import { EditOutlined, EyeOutlined } from "@ant-design/icons";
|
||||
import dayjs from "dayjs";
|
||||
@@ -106,7 +106,7 @@ export function SpoolCard({
|
||||
item: () => {
|
||||
return { spool, index };
|
||||
},
|
||||
collect: (monitor: any) => ({
|
||||
collect: (monitor: DragSourceMonitor<{ spool: ISpool; index: number }>) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
end() {
|
||||
|
||||
@@ -35,7 +35,7 @@ export function useRenameSpoolLocation() {
|
||||
},
|
||||
onMutate: async (value) => {
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: queryKeyList
|
||||
queryKey: queryKeyList,
|
||||
});
|
||||
|
||||
// Optimistically update all spools with matching location to the new one
|
||||
@@ -54,7 +54,7 @@ export function useRenameSpoolLocation() {
|
||||
return old;
|
||||
});
|
||||
},
|
||||
onError: (_error, value, _context) => {
|
||||
onError: (_error, value) => {
|
||||
// Mutation failed, reset spools with matching location to the old one
|
||||
queryClient.setQueriesData<GetListResponse<ISpool>>({ queryKey: queryKeyList }, (old) => {
|
||||
if (old) {
|
||||
@@ -71,10 +71,10 @@ export function useRenameSpoolLocation() {
|
||||
return old;
|
||||
});
|
||||
},
|
||||
onSuccess: (_data, _value) => {
|
||||
onSuccess: () => {
|
||||
// Mutation succeeded, refetch
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKey
|
||||
queryKey: queryKey,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
|
||||
import { useTranslate } from "@refinedev/core";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import React from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
|
||||
@@ -10,7 +9,7 @@ import "./locations.css";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
export const Locations: React.FC<IResourceComponentsProps> = () => {
|
||||
export const Locations = () => {
|
||||
const t = useTranslate();
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { PageHeader } from "@refinedev/antd";
|
||||
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
|
||||
import { useTranslate } from "@refinedev/core";
|
||||
import { theme } from "antd";
|
||||
import { Content } from "antd/es/layout/layout";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import React from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router";
|
||||
import SpoolQRCodePrintingDialog from "./spoolQrCodePrintingDialog";
|
||||
import SpoolSelectModal from "./spoolSelectModal";
|
||||
@@ -13,7 +12,7 @@ dayjs.extend(utc);
|
||||
|
||||
const { useToken } = theme;
|
||||
|
||||
export const Printing: React.FC<IResourceComponentsProps> = () => {
|
||||
export const Printing = () => {
|
||||
const { token } = useToken();
|
||||
const t = useTranslate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
@@ -53,10 +53,12 @@ export function useSetPrintSettings(): (spoolQRCodePrintSettings: SpoolQRCodePri
|
||||
}
|
||||
|
||||
interface GenericObject {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[key: string]: any;
|
||||
extra: { [key: string]: string };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function getTagValue(tag: string, obj: GenericObject): any {
|
||||
// Split tag by .
|
||||
const tagParts = tag.split(".");
|
||||
@@ -99,19 +101,18 @@ function applyTextFormatting(text: string): ReactElement[] {
|
||||
|
||||
export function renderLabelContents(template: string, spool: ISpool): ReactElement {
|
||||
// Find all {tags} in the template string and loop over them
|
||||
// let matches = [...template.matchAll(/(?:{(.*?))?{(.*?)}(.*?)(?:}(.*?))?/gs)];
|
||||
let matches = [...template.matchAll(/{(?:[^}{]|{[^}{]*})*}/gs)];
|
||||
const matches = [...template.matchAll(/{(?:[^}{]|{[^}{]*})*}/gs)];
|
||||
let label_text = template;
|
||||
matches.forEach((match) => {
|
||||
if ((match[0].match(/{/g) || []).length == 1) {
|
||||
let tag = match[0].replace(/[{}]/g, "");
|
||||
let tagValue = getTagValue(tag, spool);
|
||||
const tag = match[0].replace(/[{}]/g, "");
|
||||
const tagValue = getTagValue(tag, spool);
|
||||
label_text = label_text.replace(match[0], tagValue);
|
||||
} else if ((match[0].match(/{/g) || []).length == 2) {
|
||||
let structure = match[0].match(/{(.*?){(.*?)}(.*?)}/);
|
||||
const structure = match[0].match(/{(.*?){(.*?)}(.*?)}/);
|
||||
if (structure != null) {
|
||||
const tag = structure[2];
|
||||
let tagValue = getTagValue(tag, spool);
|
||||
const tagValue = getTagValue(tag, spool);
|
||||
if (tagValue == "?") {
|
||||
label_text = label_text.replace(match[0], "");
|
||||
} else {
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
Space,
|
||||
} from "antd";
|
||||
import * as htmlToImage from "html-to-image";
|
||||
import React, { ReactElement, useRef } from "react";
|
||||
import { ReactElement, useRef } from "react";
|
||||
import { useReactToPrint } from "react-to-print";
|
||||
import { useSavedState } from "../../utils/saveload";
|
||||
import { PrintSettings } from "./printing";
|
||||
@@ -62,7 +62,7 @@ const paperDimensions: { [key: string]: PaperDimensions } = {
|
||||
},
|
||||
};
|
||||
|
||||
const PrintingDialog: React.FC<PrintingDialogProps> = ({
|
||||
const PrintingDialog = ({
|
||||
items,
|
||||
printSettings,
|
||||
setPrintSettings,
|
||||
@@ -70,7 +70,7 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({
|
||||
extraSettings,
|
||||
extraSettingsStart,
|
||||
extraButtons,
|
||||
}) => {
|
||||
}: PrintingDialogProps) => {
|
||||
const t = useTranslate();
|
||||
|
||||
const [collapseState, setCollapseState] = useSavedState<string[]>("print-collapseState", []);
|
||||
|
||||
@@ -25,7 +25,7 @@ interface QRCodePrintingDialogProps {
|
||||
setUseHTTPUrl: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const QRCodePrintingDialog: React.FC<QRCodePrintingDialogProps> = ({
|
||||
const QRCodePrintingDialog = ({
|
||||
items,
|
||||
printSettings,
|
||||
setPrintSettings,
|
||||
@@ -35,16 +35,16 @@ const QRCodePrintingDialog: React.FC<QRCodePrintingDialogProps> = ({
|
||||
baseUrlRoot,
|
||||
useHTTPUrl,
|
||||
setUseHTTPUrl,
|
||||
}) => {
|
||||
}: QRCodePrintingDialogProps) => {
|
||||
const t = useTranslate();
|
||||
|
||||
const showContent = printSettings?.showContent === undefined ? true : printSettings?.showContent;
|
||||
const showQRCodeMode = printSettings?.showQRCodeMode || "withIcon";
|
||||
const textSize = printSettings?.textSize || 3;
|
||||
|
||||
const elements = items.map((item) => {
|
||||
const elements = items.map((item, idx) => {
|
||||
return (
|
||||
<div className="print-qrcode-item">
|
||||
<div className="print-qrcode-item" key={idx}>
|
||||
{showQRCodeMode !== "no" && (
|
||||
<div className="print-qrcode-container">
|
||||
<QRCode
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { CopyOutlined, DeleteOutlined, PlusOutlined, SaveOutlined } from "@ant-design/icons";
|
||||
import { useGetSetting } from "../../utils/querySettings";
|
||||
import { useTranslate } from "@refinedev/core";
|
||||
import { Button, Flex, Form, Input, Modal, Popconfirm, Select, Table, Typography, message } from "antd";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import { useState } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { EntityType, useGetFields } from "../../utils/queryFields";
|
||||
import { useGetSetting } from "../../utils/querySettings";
|
||||
import { useSavedState } from "../../utils/saveload";
|
||||
import { useGetSpoolsByIds } from "../spools/functions";
|
||||
import { ISpool } from "../spools/model";
|
||||
@@ -23,7 +23,7 @@ interface SpoolQRCodePrintingDialog {
|
||||
spoolIds: number[];
|
||||
}
|
||||
|
||||
const SpoolQRCodePrintingDialog: React.FC<SpoolQRCodePrintingDialog> = ({ spoolIds }) => {
|
||||
const SpoolQRCodePrintingDialog = ({ spoolIds }: SpoolQRCodePrintingDialog) => {
|
||||
const t = useTranslate();
|
||||
const baseUrlSetting = useGetSetting("base_url");
|
||||
const baseUrlRoot =
|
||||
@@ -86,14 +86,14 @@ const SpoolQRCodePrintingDialog: React.FC<SpoolQRCodePrintingDialog> = ({ spoolI
|
||||
if (!localOrRemotePresets) return;
|
||||
setLocalPresets(
|
||||
localOrRemotePresets.map((presets) =>
|
||||
presets.labelSettings.printSettings.id === newSettings.labelSettings.printSettings.id ? newSettings : presets
|
||||
)
|
||||
presets.labelSettings.printSettings.id === newSettings.labelSettings.printSettings.id ? newSettings : presets,
|
||||
),
|
||||
);
|
||||
};
|
||||
const deleteCurrentPreset = () => {
|
||||
if (!localOrRemotePresets) return;
|
||||
setLocalPresets(
|
||||
localOrRemotePresets.filter((qPreset) => qPreset.labelSettings.printSettings.id !== selectedPresetState)
|
||||
localOrRemotePresets.filter((qPreset) => qPreset.labelSettings.printSettings.id !== selectedPresetState),
|
||||
);
|
||||
setSelectedPresetState(undefined);
|
||||
};
|
||||
@@ -132,7 +132,7 @@ const SpoolQRCodePrintingDialog: React.FC<SpoolQRCodePrintingDialog> = ({ spoolI
|
||||
} else {
|
||||
// A setting has been selected, find it
|
||||
const foundSetting = localOrRemotePresets.find(
|
||||
(settings) => settings.labelSettings.printSettings.id === selectedPresetState
|
||||
(settings) => settings.labelSettings.printSettings.id === selectedPresetState,
|
||||
);
|
||||
if (foundSetting) {
|
||||
curPreset = foundSetting;
|
||||
|
||||
@@ -36,7 +36,7 @@ function collapseSpool(element: ISpool): ISpoolCollapsed {
|
||||
};
|
||||
}
|
||||
|
||||
const SpoolSelectModal: React.FC<Props> = ({ description, onContinue }) => {
|
||||
const SpoolSelectModal = ({ description, onContinue }: Props) => {
|
||||
const [selectedItems, setSelectedItems] = useState<number[]>([]);
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
@@ -81,7 +81,7 @@ const SpoolSelectModal: React.FC<Props> = ({ description, onContinue }) => {
|
||||
// Collapse the dataSource to a mutable list and add a filament_name field
|
||||
const dataSource: ISpoolCollapsed[] = useMemo(
|
||||
() => (tableProps.dataSource || []).map((record) => ({ ...record })),
|
||||
[tableProps.dataSource]
|
||||
[tableProps.dataSource],
|
||||
);
|
||||
|
||||
// Function to add/remove all filtered items from selected items
|
||||
@@ -95,7 +95,7 @@ const SpoolSelectModal: React.FC<Props> = ({ description, onContinue }) => {
|
||||
// Handler for selecting/unselecting individual items
|
||||
const handleSelectItem = (item: number) => {
|
||||
setSelectedItems((prevSelected) =>
|
||||
prevSelected.includes(item) ? prevSelected.filter((selected) => selected !== item) : [...prevSelected, item]
|
||||
prevSelected.includes(item) ? prevSelected.filter((selected) => selected !== item) : [...prevSelected, item],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -184,8 +184,8 @@ const SpoolSelectModal: React.FC<Props> = ({ description, onContinue }) => {
|
||||
// Remove archived spools from selected items
|
||||
setSelectedItems((prevSelected) =>
|
||||
prevSelected.filter(
|
||||
(selected) => dataSource.find((spool) => spool.id === selected)?.archived !== true
|
||||
)
|
||||
(selected) => dataSource.find((spool) => spool.id === selected)?.archived !== true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -55,7 +55,7 @@ const canEditField = (dataIndex: string, isNew: boolean) => {
|
||||
return dataIndex !== "key" && dataIndex !== "field_type" && dataIndex !== "multi_choice";
|
||||
};
|
||||
|
||||
const EditableCell: React.FC<EditableCellProps> = ({ record, editing, dataIndex, children, form, ...restProps }) => {
|
||||
const EditableCell = ({ record, editing, dataIndex, children, form, ...restProps }: EditableCellProps) => {
|
||||
const t = useTranslate();
|
||||
|
||||
if (!editing || !canEditField(dataIndex, record.is_new)) {
|
||||
@@ -235,7 +235,7 @@ const EditableCell: React.FC<EditableCellProps> = ({ record, editing, dataIndex,
|
||||
throw new Error(
|
||||
t("settings.extra_fields.choices_missing_error", {
|
||||
choices: missingChoices.join(", "),
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -360,7 +360,7 @@ export function ExtraFieldsSettings() {
|
||||
let row;
|
||||
try {
|
||||
row = (await form.validateFields()) as Field;
|
||||
} catch (errInfo) {
|
||||
} catch {
|
||||
// Ignore these errors because they are already handled by the form
|
||||
return;
|
||||
}
|
||||
@@ -488,12 +488,11 @@ export function ExtraFieldsSettings() {
|
||||
} else if (Array.isArray(val) && record.field.field_type === FieldType.choice) {
|
||||
return val.join(", ");
|
||||
} else if (
|
||||
Array.isArray(val) && (
|
||||
record.field.field_type === FieldType.integer_range || record.field.field_type === FieldType.float_range
|
||||
)
|
||||
Array.isArray(val) &&
|
||||
(record.field.field_type === FieldType.integer_range || record.field.field_type === FieldType.float_range)
|
||||
) {
|
||||
let lower = val[0] ?? "";
|
||||
let upper = val[1] ?? "";
|
||||
const lower = val[0] ?? "";
|
||||
const upper = val[1] ?? "";
|
||||
if (lower === "" && upper === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { FileOutlined, HighlightOutlined, SolutionOutlined, ToolOutlined, UserOutlined } from "@ant-design/icons";
|
||||
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
|
||||
import { useTranslate } from "@refinedev/core";
|
||||
import { Menu, theme } from "antd";
|
||||
import { Content } from "antd/es/layout/layout";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import React from "react";
|
||||
import { Route, Routes, useNavigate } from "react-router";
|
||||
import { ExtraFieldsSettings } from "./extraFieldsSettings";
|
||||
import { GeneralSettings } from "./generalSettings";
|
||||
@@ -13,7 +12,7 @@ dayjs.extend(utc);
|
||||
|
||||
const { useToken } = theme;
|
||||
|
||||
export const Settings: React.FC<IResourceComponentsProps> = () => {
|
||||
export const Settings = () => {
|
||||
const { token } = useToken();
|
||||
const t = useTranslate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -27,7 +27,7 @@ type ISpoolRequest = Omit<ISpoolParsedExtras, "id" | "registered"> & {
|
||||
filament_id: number | string;
|
||||
};
|
||||
|
||||
export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps> = (props) => {
|
||||
export const SpoolCreate = (props: IResourceComponentsProps & CreateOrCloneProps) => {
|
||||
const t = useTranslate();
|
||||
const extraFields = useGetFields(EntityType.spool);
|
||||
const currency = useCurrency();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Edit, useForm } from "@refinedev/antd";
|
||||
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
|
||||
import { HttpError, useTranslate } from "@refinedev/core";
|
||||
import { Alert, DatePicker, Divider, Form, Input, InputNumber, Radio, Select, Typography } from "antd";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import { message } from "antd/lib";
|
||||
@@ -28,13 +28,13 @@ type ISpoolRequest = ISpoolParsedExtras & {
|
||||
filament_id: number | string;
|
||||
};
|
||||
|
||||
export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
|
||||
export const SpoolEdit = () => {
|
||||
const t = useTranslate();
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [hasChanged, setHasChanged] = useState(false);
|
||||
const extraFields = useGetFields(EntityType.spool);
|
||||
const currency = useCurrency();
|
||||
const [searchParams, _] = useSearchParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { form, formProps, saveButtonProps } = useForm<ISpool, HttpError, ISpoolRequest, ISpool>({
|
||||
|
||||
@@ -94,7 +94,7 @@ export function formatFilamentLabel(
|
||||
vendorName?: string,
|
||||
material?: string,
|
||||
weight?: number,
|
||||
spoolType?: SpoolType
|
||||
spoolType?: SpoolType,
|
||||
): string {
|
||||
const portions = [];
|
||||
if (vendorName) {
|
||||
@@ -141,7 +141,7 @@ export function useGetFilamentSelectOptions() {
|
||||
item.diameter,
|
||||
item.vendor?.name,
|
||||
item.material,
|
||||
item.weight
|
||||
item.weight,
|
||||
),
|
||||
value: item.id,
|
||||
weight: item.weight,
|
||||
@@ -164,7 +164,7 @@ export function useGetFilamentSelectOptions() {
|
||||
item.manufacturer,
|
||||
item.material,
|
||||
item.weight,
|
||||
item.spool_type
|
||||
item.spool_type,
|
||||
),
|
||||
value: item.id,
|
||||
weight: item.weight,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
ToTopOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { List, useTable } from "@refinedev/antd";
|
||||
import { IResourceComponentsProps, useInvalidate, useNavigation, useTranslate } from "@refinedev/core";
|
||||
import { useInvalidate, useNavigation, useTranslate } from "@refinedev/core";
|
||||
import { Button, Dropdown, Modal, Table } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
@@ -94,10 +94,10 @@ const allColumns: (keyof ISpoolCollapsed & string)[] = [
|
||||
"comment",
|
||||
];
|
||||
const defaultColumns = allColumns.filter(
|
||||
(column_id) => ["registered", "used_length", "remaining_length", "lot_nr"].indexOf(column_id) === -1
|
||||
(column_id) => ["registered", "used_length", "remaining_length", "lot_nr"].indexOf(column_id) === -1,
|
||||
);
|
||||
|
||||
export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
||||
export const SpoolList = () => {
|
||||
const t = useTranslate();
|
||||
const invalidate = useInvalidate();
|
||||
const navigate = useNavigate();
|
||||
@@ -173,7 +173,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
||||
// Collapse the dataSource to a mutable list
|
||||
const queryDataSource: ISpoolCollapsed[] = useMemo(
|
||||
() => (tableProps.dataSource || []).map((record) => ({ ...record })),
|
||||
[tableProps.dataSource]
|
||||
[tableProps.dataSource],
|
||||
);
|
||||
const dataSource = useLiveify("spool", queryDataSource, collapseSpool);
|
||||
|
||||
@@ -229,7 +229,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
||||
}
|
||||
return actions;
|
||||
},
|
||||
[t, editUrl, showUrl, cloneUrl, openSpoolAdjustModal, archiveSpool, archiveSpoolPopup]
|
||||
[t, editUrl, showUrl, cloneUrl, openSpoolAdjustModal, archiveSpool, archiveSpoolPopup],
|
||||
);
|
||||
|
||||
const originalOnChange = tableProps.onChange;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { InboxOutlined, PrinterOutlined, ToTopOutlined, ToolOutlined } from "@ant-design/icons";
|
||||
import { DateField, NumberField, Show, TextField } from "@refinedev/antd";
|
||||
import { IResourceComponentsProps, useInvalidate, useShow, useTranslate } from "@refinedev/core";
|
||||
import { useInvalidate, useShow, useTranslate } from "@refinedev/core";
|
||||
import { Button, Modal, Typography } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import React from "react";
|
||||
import { ExtraFieldDisplay } from "../../components/extraFields";
|
||||
import { NumberFieldUnit } from "../../components/numberField";
|
||||
import SpoolIcon from "../../components/spoolIcon";
|
||||
@@ -21,7 +20,7 @@ dayjs.extend(utc);
|
||||
const { Title } = Typography;
|
||||
const { confirm } = Modal;
|
||||
|
||||
export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
|
||||
export const SpoolShow = () => {
|
||||
const t = useTranslate();
|
||||
const extraFields = useGetFields(EntityType.spool);
|
||||
const currencyFormatter = useCurrencyFormatter();
|
||||
|
||||
Vendored
+1
-1
@@ -15,7 +15,7 @@ interface CreateOrCloneProps {
|
||||
mode: "create" | "clone";
|
||||
}
|
||||
|
||||
export const VendorCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps> = (props) => {
|
||||
export const VendorCreate = (props: IResourceComponentsProps & CreateOrCloneProps) => {
|
||||
const t = useTranslate();
|
||||
const extraFields = useGetFields(EntityType.vendor);
|
||||
|
||||
|
||||
Vendored
+10
-10
@@ -1,9 +1,9 @@
|
||||
import { Edit, useForm } from "@refinedev/antd";
|
||||
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
|
||||
import { HttpError, useTranslate } from "@refinedev/core";
|
||||
import { Alert, DatePicker, Form, Input, InputNumber, message, Typography } from "antd";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import dayjs from "dayjs";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
|
||||
import { EntityType, useGetFields } from "../../utils/queryFields";
|
||||
import { IVendor, IVendorParsedExtras } from "./model";
|
||||
@@ -15,7 +15,7 @@ We also need to stringify them again before sending them back to the API, which
|
||||
the form's onFinish method. Form.Item's normalize should do this, but it doesn't seem to work.
|
||||
*/
|
||||
|
||||
export const VendorEdit: React.FC<IResourceComponentsProps> = () => {
|
||||
export const VendorEdit = () => {
|
||||
const t = useTranslate();
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [hasChanged, setHasChanged] = useState(false);
|
||||
@@ -114,13 +114,13 @@ export const VendorEdit: React.FC<IResourceComponentsProps> = () => {
|
||||
<InputNumber addonAfter="g" precision={1} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("vendor.fields.external_id")}
|
||||
name={["external_id"]}
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
},
|
||||
]}
|
||||
label={t("vendor.fields.external_id")}
|
||||
name={["external_id"]}
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input maxLength={64} />
|
||||
</Form.Item>
|
||||
|
||||
Vendored
+3
-3
@@ -1,6 +1,6 @@
|
||||
import { EditOutlined, EyeOutlined, FilterOutlined, PlusSquareOutlined } from "@ant-design/icons";
|
||||
import { List, useTable } from "@refinedev/antd";
|
||||
import { IResourceComponentsProps, useInvalidate, useNavigation, useTranslate } from "@refinedev/core";
|
||||
import { useInvalidate, useNavigation, useTranslate } from "@refinedev/core";
|
||||
import { Button, Dropdown, Table } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
@@ -26,7 +26,7 @@ const namespace = "vendorList-v2";
|
||||
|
||||
const allColumns: (keyof IVendor & string)[] = ["id", "name", "registered", "comment", "empty_spool_weight"];
|
||||
|
||||
export const VendorList: React.FC<IResourceComponentsProps> = () => {
|
||||
export const VendorList = () => {
|
||||
const t = useTranslate();
|
||||
const invalidate = useInvalidate();
|
||||
const navigate = useNavigate();
|
||||
@@ -85,7 +85,7 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
|
||||
const dataSource = useLiveify(
|
||||
"vendor",
|
||||
queryDataSource,
|
||||
useCallback((record: IVendor) => record, [])
|
||||
useCallback((record: IVendor) => record, []),
|
||||
);
|
||||
|
||||
if (tableProps.pagination) {
|
||||
|
||||
Vendored
+2
-3
@@ -1,9 +1,8 @@
|
||||
import { DateField, NumberField, Show, TextField } from "@refinedev/antd";
|
||||
import { IResourceComponentsProps, useShow, useTranslate } from "@refinedev/core";
|
||||
import { useShow, useTranslate } from "@refinedev/core";
|
||||
import { Typography } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import React from "react";
|
||||
import { ExtraFieldDisplay } from "../../components/extraFields";
|
||||
import { enrichText } from "../../utils/parsing";
|
||||
import { EntityType, useGetFields } from "../../utils/queryFields";
|
||||
@@ -13,7 +12,7 @@ dayjs.extend(utc);
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
export const VendorShow: React.FC<IResourceComponentsProps> = () => {
|
||||
export const VendorShow = () => {
|
||||
const t = useTranslate();
|
||||
const extraFields = useGetFields(EntityType.vendor);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export function typeFilters<Obj>(filters: CrudFilter[]): TypedCrudFilter<Obj>[]
|
||||
*/
|
||||
export function getFiltersForField<Obj, Field extends keyof Obj>(
|
||||
filters: TypedCrudFilter<Obj>[],
|
||||
field: Field
|
||||
field: Field,
|
||||
): string[] {
|
||||
const filterValues: string[] = [];
|
||||
filters.forEach((filter) => {
|
||||
|
||||
@@ -32,7 +32,7 @@ export function formatNumberWithSpaceSeparator(input: string): string {
|
||||
*/
|
||||
export function formatNumberOnUserInput(
|
||||
value: number | string | undefined,
|
||||
info: { userTyping: boolean; input: string }
|
||||
info: { userTyping: boolean; input: string },
|
||||
): string {
|
||||
if (info.userTyping) {
|
||||
return info.input;
|
||||
|
||||
@@ -67,7 +67,7 @@ export function useSetField(entity_type: EntityType) {
|
||||
onMutate: async ({ key, params }) => {
|
||||
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: ["fields", entity_type]
|
||||
queryKey: ["fields", entity_type],
|
||||
});
|
||||
|
||||
// Snapshot the previous value
|
||||
@@ -104,7 +104,7 @@ export function useSetField(entity_type: EntityType) {
|
||||
onSettled: () => {
|
||||
// Invalidate and refetch
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["fields", entity_type]
|
||||
queryKey: ["fields", entity_type],
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -129,7 +129,7 @@ export function useDeleteField(entity_type: EntityType) {
|
||||
onSuccess: () => {
|
||||
// Invalidate and refetch
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["fields", entity_type]
|
||||
queryKey: ["fields", entity_type],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -53,21 +53,21 @@ export function useSetSetting<T>(key: string) {
|
||||
},
|
||||
onMutate: async (value) => {
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: ["settings", key]
|
||||
queryKey: ["settings", key],
|
||||
});
|
||||
const previousValue = queryClient.getQueryData<SettingResponseValue>(["settings", key]);
|
||||
queryClient.setQueryData<SettingResponseValue>(["settings", key], (old) =>
|
||||
old ? { ...old, value: JSON.stringify(value) } : undefined
|
||||
old ? { ...old, value: JSON.stringify(value) } : undefined,
|
||||
);
|
||||
return previousValue;
|
||||
},
|
||||
onError: (_error, _value, context) => {
|
||||
queryClient.setQueryData<SettingResponseValue>(["settings", key], context);
|
||||
},
|
||||
onSuccess: (_data, _value) => {
|
||||
onSuccess: () => {
|
||||
// Invalidate and refetch
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["settings", key]
|
||||
queryKey: ["settings", key],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -18,18 +18,18 @@ export function useInitialTableState(tableId: string): TableState {
|
||||
const savedSorters = hasHashProperty("sorters")
|
||||
? getHashProperty("sorters")
|
||||
: isLocalStorageAvailable
|
||||
? localStorage.getItem(`${tableId}-sorters`)
|
||||
: null;
|
||||
? localStorage.getItem(`${tableId}-sorters`)
|
||||
: null;
|
||||
const savedFilters = hasHashProperty("filters")
|
||||
? getHashProperty("filters")
|
||||
: isLocalStorageAvailable
|
||||
? localStorage.getItem(`${tableId}-filters`)
|
||||
: null;
|
||||
? localStorage.getItem(`${tableId}-filters`)
|
||||
: null;
|
||||
const savedPagination = hasHashProperty("pagination")
|
||||
? getHashProperty("pagination")
|
||||
: isLocalStorageAvailable
|
||||
? localStorage.getItem(`${tableId}-pagination`)
|
||||
: null;
|
||||
? localStorage.getItem(`${tableId}-pagination`)
|
||||
: null;
|
||||
const savedShowColumns = isLocalStorageAvailable ? localStorage.getItem(`${tableId}-showColumns`) : null;
|
||||
|
||||
const sorters = savedSorters ? JSON.parse(savedSorters) : [{ field: "id", order: "asc" }];
|
||||
|
||||
@@ -14,7 +14,7 @@ interface TypedCrudSort<Obj> {
|
||||
*/
|
||||
export function getSortOrderForField<Obj, Field extends keyof Obj>(
|
||||
sorters: TypedCrudSort<Obj>[],
|
||||
field: Field
|
||||
field: Field,
|
||||
): SortOrder | undefined {
|
||||
const sorter = sorters.find((s) => s.field === field);
|
||||
if (sorter) {
|
||||
|
||||
@@ -3,7 +3,7 @@ function _isLocalStorageAvailable(): boolean {
|
||||
localStorage.setItem("test", "test");
|
||||
localStorage.removeItem("test");
|
||||
return true;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
pre-commit:
|
||||
parallel: true
|
||||
jobs:
|
||||
- group: # Backend checks
|
||||
parallel: false
|
||||
jobs:
|
||||
- name: Run lint check on backend files
|
||||
run: uv run ruff check --fix .
|
||||
stage_fixed: true
|
||||
- name: Run format check on backend files
|
||||
run: uv run ruff format .
|
||||
stage_fixed: true
|
||||
- name: Check UV lockfile consistency
|
||||
run: uv lock --check
|
||||
- group: # Client checks
|
||||
parallel: false
|
||||
jobs:
|
||||
- name: Run lint check on client files
|
||||
root: "client/"
|
||||
run: ./node_modules/.bin/eslint --fix . # Hardcoded path because Windows messes up if we use npx
|
||||
stage_fixed: true
|
||||
- name: Run format check on client files
|
||||
root: "client/"
|
||||
run: ./node_modules/.bin/prettier --write .
|
||||
stage_fixed: true
|
||||
|
||||
# Same as pre-commit but no fixes
|
||||
ci:
|
||||
parallel: true
|
||||
jobs:
|
||||
- group: # Backend checks
|
||||
parallel: false
|
||||
jobs:
|
||||
- name: Run lint check on backend files
|
||||
run: uv run ruff check .
|
||||
- name: Run format check on backend files
|
||||
run: uv run ruff format --check .
|
||||
- name: Check UV lockfile consistency
|
||||
run: uv lock --check
|
||||
- group: # Client checks
|
||||
parallel: false
|
||||
jobs:
|
||||
- name: Run lint check on client files
|
||||
root: "client/"
|
||||
run: ./node_modules/.bin/eslint . # Hardcoded path because Windows messes up if we use npx
|
||||
- name: Run format check on client files
|
||||
root: "client/"
|
||||
run: ./node_modules/.bin/prettier --check .
|
||||
|
||||
+37
-32
@@ -2,6 +2,8 @@
|
||||
name = "spoolman"
|
||||
version = "0.22.1"
|
||||
description = "A web service that keeps track of 3D printing spools."
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
{ name = "Donkie", email = "daniel.cf.hultgren@gmail.com" },
|
||||
]
|
||||
@@ -24,36 +26,48 @@ dependencies = [
|
||||
"httpx~=0.28",
|
||||
"hishel~=0.1",
|
||||
]
|
||||
requires-python = ">=3.9,<3.13"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[project.license]
|
||||
text = "MIT"
|
||||
|
||||
[tool.pdm.dev-dependencies]
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ruff==0.11.0",
|
||||
"black~=25.1.0",
|
||||
"pre-commit~=4.1.0",
|
||||
"ruff==0.14.10",
|
||||
"pytest~=8.3.5",
|
||||
"pytest-asyncio~=0.25.3",
|
||||
"httpx~=0.28.1",
|
||||
"poethepoet>=0.40.0",
|
||||
"bump2version>=1.0.1",
|
||||
]
|
||||
|
||||
[tool.pdm.scripts.docs]
|
||||
call = "spoolman.docs:generate_docs"
|
||||
[tool.poe.tasks.generate-docs]
|
||||
script = "spoolman.docs:generate_docs"
|
||||
help = "Generate API documentation for Spoolman."
|
||||
|
||||
[tool.pdm.scripts.bump]
|
||||
call = "spoolman.bump:bump"
|
||||
[tool.poe.tasks.bump]
|
||||
script = "spoolman.bump:bump"
|
||||
help = "Bump Spoolman version."
|
||||
|
||||
[tool.pdm.scripts.app]
|
||||
[tool.poe.tasks.run]
|
||||
cmd = "uvicorn spoolman.main:app"
|
||||
help = "Start Spoolman."
|
||||
|
||||
[tool.pdm.scripts.itest]
|
||||
[tool.poe.tasks.itest]
|
||||
cmd = "python tests_integration/run.py"
|
||||
help = "Builds Spoolman and runs integration tests on the backend against all supported databases."
|
||||
|
||||
[tool.bumpversion]
|
||||
current_version = "0.22.1"
|
||||
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
||||
serialize = ["{major}.{minor}.{patch}"]
|
||||
allow_dirty = false
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "pyproject.toml"
|
||||
search = 'version = "{current_version}"'
|
||||
replace = 'version = "{new_version}"'
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py39"
|
||||
target-version = "py310"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
@@ -61,24 +75,26 @@ select = [
|
||||
]
|
||||
ignore = [
|
||||
"A003",
|
||||
"COM812",
|
||||
"D101",
|
||||
"D104",
|
||||
"D203",
|
||||
"D213",
|
||||
"D406",
|
||||
"D407",
|
||||
"FA100",
|
||||
"FIX002",
|
||||
"S104",
|
||||
"TRY201",
|
||||
"TRY003",
|
||||
"DTZ003",
|
||||
"EM101",
|
||||
"EM102",
|
||||
"DTZ003",
|
||||
"FA100",
|
||||
"FIX002",
|
||||
"PLR0913",
|
||||
"S104",
|
||||
"S603",
|
||||
"SIM108",
|
||||
"TD002",
|
||||
"TD003",
|
||||
"TRY003",
|
||||
"TRY201",
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
@@ -94,14 +110,3 @@ ignore = [
|
||||
"N999",
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
target-version = [
|
||||
"py39",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = [
|
||||
"pdm-backend",
|
||||
]
|
||||
build-backend = "pdm.backend"
|
||||
+32
-109
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/bin/bash -e
|
||||
|
||||
# ANSI color codes
|
||||
GREEN='\033[0;32m'
|
||||
@@ -28,32 +28,30 @@ if [ "$(basename "$current_dir")" = "scripts" ]; then
|
||||
fi
|
||||
|
||||
#
|
||||
# Python version verification
|
||||
# Install uv if not installed
|
||||
#
|
||||
if ! command -v python3 &>/dev/null; then
|
||||
echo -e "${ORANGE}Python 3 is not installed or not found. Please install at least Python 3.9 before you continue.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
local_uv_dir="$(pwd)/uv"
|
||||
local_uv_bin="$local_uv_dir/uv"
|
||||
|
||||
python_version=$(python3 --version) || exit 1
|
||||
version_number=$(echo "$python_version" | awk '{print $2}')
|
||||
IFS='.' read -r major minor patch <<< "$version_number"
|
||||
|
||||
if [[ "$major" -eq 3 && "$minor" -ge 9 ]]; then
|
||||
echo -e "${GREEN}Python 3.9 or later is installed (Current version: $version_number)${NC}"
|
||||
if command -v uv &> /dev/null; then
|
||||
echo "uv found in PATH. Using system uv."
|
||||
else
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
source /etc/os-release
|
||||
if [[ "$VERSION_CODENAME" == "buster" ]]; then
|
||||
echo -e "${ORANGE}Python 3.9 or later is not installed (Current version: $version_number)${NC}"
|
||||
echo -e "${ORANGE}You are running an outdated version of Debian/Raspbian (Buster). If you upgrade to Bullseye, you will get the correct python version. Please see guides online on how to upgrade your operating system.${NC}"
|
||||
exit 1
|
||||
if [ -x "$local_uv_bin" ]; then
|
||||
if "$local_uv_bin" --version &> /dev/null; then
|
||||
echo "Using local uv from $local_uv_dir"
|
||||
export PATH="$local_uv_dir:$PATH"
|
||||
else
|
||||
echo "Local uv found but failed to run. Installing temporary uv..."
|
||||
curl -LsSf https://astral.sh/uv/install.sh | env UV_UNMANAGED_INSTALL="$local_uv_dir" sh
|
||||
export PATH="$local_uv_dir:$PATH"
|
||||
fi
|
||||
else
|
||||
echo "Installing temporary uv..."
|
||||
curl -LsSf https://astral.sh/uv/install.sh | env UV_UNMANAGED_INSTALL="$local_uv_dir" sh
|
||||
export PATH="$local_uv_dir:$PATH"
|
||||
fi
|
||||
echo -e "${ORANGE}Current version of Python ($version_number) is too old for Spoolman.${NC}"
|
||||
echo -e "${ORANGE}Please look up how to install Python 3.9 or later for your specific operating system.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#
|
||||
# Get os package manager
|
||||
#
|
||||
@@ -70,120 +68,42 @@ if [[ -f /etc/os-release ]]; then
|
||||
install_cmd="$SUDO $pkg_manager -S --noconfirm"
|
||||
echo -e "${GREEN}Detected Arch-based system. Using pacman package manager.${NC}"
|
||||
else
|
||||
echo -e "${ORANGE}Your operating system is not supported. Either try to install manually or reach out to spoolman github for support.${NC}"
|
||||
exit 1
|
||||
echo -e "${ORANGE}Unsupported Linux distribution. Please install the required dependencies manually.${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run pkg manager update
|
||||
echo -e "${GREEN}Updating $pkg_manager cache...${NC}"
|
||||
$update_cmd || exit 1
|
||||
packages=""
|
||||
if ! python3 -c 'import venv, ensurepip' &>/dev/null; then
|
||||
echo -e "${ORANGE}Python venv module is not accessible. Installing venv...${NC}"
|
||||
if [[ "$pkg_manager" == "apt-get" ]]; then
|
||||
packages+=" python3-venv"
|
||||
elif [[ "$pkg_manager" == "pacman" ]]; then
|
||||
packages+=" python-virtualenv"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! command -v pip3 &>/dev/null; then
|
||||
echo -e "${ORANGE}Python pip is not installed. Installing pip...${NC}"
|
||||
if [[ "$pkg_manager" == "apt-get" ]]; then
|
||||
packages+=" python3-pip"
|
||||
elif [[ "$pkg_manager" == "pacman" ]]; then
|
||||
packages+=" python-pip"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! command -v pg_config &>/dev/null; then
|
||||
echo -e "${ORANGE}pg_config is not available. Installing libpq-dev...${NC}"
|
||||
if [[ "$pkg_manager" == "apt-get" ]]; then
|
||||
packages+=" libpq-dev"
|
||||
elif [[ "$pkg_manager" == "pacman" ]]; then
|
||||
packages+=" postgresql-libs"
|
||||
else
|
||||
echo -e "${ORANGE}pg_config not found and automatic installation not supported for this OS. Please install libpq-dev or postgresql-libs manually.${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! command -v unzip &>/dev/null; then
|
||||
echo -e "${ORANGE}unzip is not available. Installing unzip...${NC}"
|
||||
packages+=" unzip"
|
||||
fi
|
||||
# not needed?
|
||||
# if ! command -v unzip &>/dev/null; then
|
||||
# echo -e "${ORANGE}unzip is not available. Installing unzip...${NC}"
|
||||
# packages+=" unzip"
|
||||
# fi
|
||||
|
||||
if [[ -n "$packages" ]]; then
|
||||
$update_cmd || exit 1
|
||||
$install_cmd $packages || exit 1
|
||||
fi
|
||||
|
||||
#
|
||||
# Update pip
|
||||
#
|
||||
echo -e "${GREEN}Updating pip...${NC}"
|
||||
|
||||
# Run pip upgrade command and capture stdout
|
||||
upgrade_output=$(python3 -m pip install --user --upgrade pip 2>&1)
|
||||
exit_code=$?
|
||||
|
||||
# Check if the upgrade command failed and contains the specified error message
|
||||
is_externally_managed_env=$(echo "$upgrade_output" | grep "error: externally-managed-environment")
|
||||
if [[ $exit_code -ne 0 ]]; then
|
||||
if [[ $is_externally_managed_env ]]; then
|
||||
echo -e "${GREEN}Warning:${NC} Failed to upgrade pip since it's version is managed by the OS. Continuing anyway..."
|
||||
else
|
||||
echo -e "${GREEN}Error:${NC} Pip upgrade failed with exit code $exit_code and the following output:"
|
||||
echo "$upgrade_output"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}Pip updated successfully.${NC}"
|
||||
fi
|
||||
|
||||
#
|
||||
# Install various pip packages if needed
|
||||
#
|
||||
echo -e "${GREEN}Installing system-wide pip packages needed for Spoolman...${NC}"
|
||||
if [[ $is_externally_managed_env ]]; then
|
||||
echo -e "${GREEN}Installing the packages using apt-get instead of pip since pip is externally managed...${NC}"
|
||||
if [[ "$pkg_manager" == "apt-get" ]]; then
|
||||
packages="python3-setuptools python3-wheel"
|
||||
elif [[ "$pkg_manager" == "pacman" ]]; then
|
||||
packages="python-setuptools python-wheel"
|
||||
fi
|
||||
$install_cmd $packages || exit 1
|
||||
else
|
||||
packages="setuptools wheel"
|
||||
for package in $packages; do
|
||||
if ! pip3 show "$package" &>/dev/null; then
|
||||
echo -e "${GREEN}Installing $package...${NC}"
|
||||
pip3 install --user "$package" || exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
#
|
||||
# Add python bin dir to PATH if needed
|
||||
#
|
||||
user_python_bin_dir=$(python3 -m site --user-base)/bin
|
||||
if [[ ! "$PATH" =~ "$user_python_bin_dir" ]]; then
|
||||
export PATH=$user_python_bin_dir:$PATH
|
||||
fi
|
||||
|
||||
#
|
||||
# Install Spoolman
|
||||
#
|
||||
|
||||
# Install dependencies
|
||||
echo -e "${GREEN}Installing Spoolman backend and its dependencies...${NC}"
|
||||
# Create venv if it doesn't exist
|
||||
if [ ! -d ".venv" ]; then
|
||||
python3 -m venv .venv || exit 1
|
||||
fi
|
||||
|
||||
# Activate venv
|
||||
source .venv/bin/activate || exit 1
|
||||
|
||||
# Install dependencies using pip
|
||||
pip3 install -r requirements.txt || exit 1
|
||||
uv sync --no-dev
|
||||
|
||||
#
|
||||
# Initialize the .env file if it doesn't exist
|
||||
@@ -207,6 +127,9 @@ if [ "$systemd_option" == "-systemd=no" ]; then
|
||||
choice="n"
|
||||
elif [ "$systemd_option" == "-systemd=yes" ]; then
|
||||
choice="y"
|
||||
elif ! command -v systemctl &> /dev/null; then
|
||||
echo -e "${ORANGE}systemctl not found. Skipping systemd service installation.${NC}"
|
||||
choice="n"
|
||||
else
|
||||
echo -e "${CYAN}Do you want to install Spoolman as a systemd service? This will automatically start Spoolman when your server starts. (y/n)${NC}"
|
||||
read choice
|
||||
|
||||
@@ -19,17 +19,6 @@ if [ ! -d ".venv" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#
|
||||
# Add python bin dir to PATH if needed
|
||||
#
|
||||
user_python_bin_dir=$(python3 -m site --user-base)/bin
|
||||
if [[ ! "$PATH" =~ "$user_python_bin_dir" ]]; then
|
||||
echo -e "${ORANGE}WARNING: $user_python_bin_dir is not in PATH, this will make it difficult to run PDM commands. Temporarily adding $user_python_bin_dir to PATH...${NC}"
|
||||
echo -e "${ORANGE}To make this permanent, add the following line to your .bashrc or .zshrc file:${NC}"
|
||||
echo -e "${ORANGE}export PATH=$user_python_bin_dir:\$PATH${NC}"
|
||||
export PATH=$user_python_bin_dir:$PATH
|
||||
fi
|
||||
|
||||
#
|
||||
# Activate .venv
|
||||
#
|
||||
|
||||
@@ -13,7 +13,7 @@ from spoolman.database.database import get_db_session
|
||||
from spoolman.database.models import Base
|
||||
from spoolman.export import dump_as_csv, dump_as_json
|
||||
|
||||
# ruff: noqa: D103,B008
|
||||
# ruff: noqa: D103
|
||||
router = APIRouter(
|
||||
prefix="/export",
|
||||
tags=["export"],
|
||||
@@ -35,7 +35,6 @@ async def export_spools(
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
fmt: ExportFormat,
|
||||
) -> Response:
|
||||
|
||||
all_spools, _ = await spool.find(db=db)
|
||||
return await _export(all_spools, fmt)
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ router = APIRouter(
|
||||
tags=["external"],
|
||||
)
|
||||
|
||||
# ruff: noqa: D103,B008
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Vendor related endpoints."""
|
||||
|
||||
import logging
|
||||
from typing import Annotated, Union
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Path
|
||||
from fastapi.responses import JSONResponse
|
||||
@@ -24,7 +24,7 @@ router = APIRouter(
|
||||
tags=["field"],
|
||||
)
|
||||
|
||||
# ruff: noqa: D103,B008
|
||||
# ruff: noqa: D103
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -58,7 +58,7 @@ async def update(
|
||||
entity_type: Annotated[EntityType, Path(description="Entity type this field is for")],
|
||||
key: Annotated[str, Path(min_length=1, max_length=64, regex="^[a-z0-9_]+$")],
|
||||
body: ExtraFieldParameters,
|
||||
) -> Union[list[ExtraField], JSONResponse]:
|
||||
) -> list[ExtraField] | JSONResponse:
|
||||
dict_body = body.model_dump()
|
||||
dict_body["key"] = key
|
||||
dict_body["entity_type"] = entity_type
|
||||
@@ -76,8 +76,7 @@ async def update(
|
||||
"/{entity_type}/{key}",
|
||||
name="Delete extra field",
|
||||
description=(
|
||||
"Delete an extra field for a specific entity type. "
|
||||
"Returns the full list of extra fields for the entity type."
|
||||
"Delete an extra field for a specific entity type. Returns the full list of extra fields for the entity type."
|
||||
),
|
||||
response_model_exclude_none=True,
|
||||
response_model=list[ExtraField],
|
||||
@@ -87,7 +86,7 @@ async def delete(
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
entity_type: Annotated[EntityType, Path(description="Entity type this field is for")],
|
||||
key: Annotated[str, Path(min_length=1, max_length=64, regex="^[a-z0-9_]+$")],
|
||||
) -> Union[list[ExtraField], JSONResponse]:
|
||||
) -> list[ExtraField] | JSONResponse:
|
||||
try:
|
||||
await delete_extra_field(db, entity_type, key)
|
||||
except ItemNotFoundError:
|
||||
|
||||
+33
-33
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Annotated, Optional
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, WebSocket, WebSocketDisconnect
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
@@ -25,11 +25,11 @@ router = APIRouter(
|
||||
tags=["filament"],
|
||||
)
|
||||
|
||||
# ruff: noqa: D103, B008
|
||||
# ruff: noqa: D103
|
||||
|
||||
|
||||
class FilamentParameters(BaseModel):
|
||||
name: Optional[str] = Field(
|
||||
name: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description=(
|
||||
@@ -38,14 +38,14 @@ class FilamentParameters(BaseModel):
|
||||
),
|
||||
examples=["PolyTerra™ Charcoal Black"],
|
||||
)
|
||||
vendor_id: Optional[int] = Field(None, description="The ID of the vendor of this filament type.")
|
||||
material: Optional[str] = Field(
|
||||
vendor_id: int | None = Field(None, description="The ID of the vendor of this filament type.")
|
||||
material: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="The material of this filament, e.g. PLA.",
|
||||
examples=["PLA"],
|
||||
)
|
||||
price: Optional[float] = Field(
|
||||
price: float | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description="The price of this filament in the system configured currency.",
|
||||
@@ -53,38 +53,38 @@ class FilamentParameters(BaseModel):
|
||||
)
|
||||
density: float = Field(gt=0, description="The density of this filament in g/cm3.", examples=[1.24])
|
||||
diameter: float = Field(gt=0, description="The diameter of this filament in mm.", examples=[1.75])
|
||||
weight: Optional[float] = Field(
|
||||
weight: float | None = Field(
|
||||
None,
|
||||
gt=0,
|
||||
description="The weight of the filament in a full spool, in grams. (net weight)",
|
||||
examples=[1000],
|
||||
)
|
||||
spool_weight: Optional[float] = Field(None, ge=0, description="The empty spool weight, in grams.", examples=[140])
|
||||
article_number: Optional[str] = Field(
|
||||
spool_weight: float | None = Field(None, ge=0, description="The empty spool weight, in grams.", examples=[140])
|
||||
article_number: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Vendor article number, e.g. EAN, QR code, etc.",
|
||||
examples=["PM70820"],
|
||||
)
|
||||
comment: Optional[str] = Field(
|
||||
comment: str | None = Field(
|
||||
None,
|
||||
max_length=1024,
|
||||
description="Free text comment about this filament type.",
|
||||
examples=[""],
|
||||
)
|
||||
settings_extruder_temp: Optional[int] = Field(
|
||||
settings_extruder_temp: int | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description="Overridden extruder temperature, in °C.",
|
||||
examples=[210],
|
||||
)
|
||||
settings_bed_temp: Optional[int] = Field(
|
||||
settings_bed_temp: int | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description="Overridden bed temperature, in °C.",
|
||||
examples=[60],
|
||||
)
|
||||
color_hex: Optional[str] = Field(
|
||||
color_hex: str | None = Field(
|
||||
None,
|
||||
description=(
|
||||
"Hexadecimal color code of the filament, e.g. FF0000 for red. Supports alpha channel at the end. "
|
||||
@@ -92,7 +92,7 @@ class FilamentParameters(BaseModel):
|
||||
),
|
||||
examples=["FF0000"],
|
||||
)
|
||||
multi_color_hexes: Optional[str] = Field(
|
||||
multi_color_hexes: str | None = Field(
|
||||
None,
|
||||
description=(
|
||||
"Hexadecimal color code of the filament, e.g. FF0000 for red. Supports alpha channel at the end. "
|
||||
@@ -101,12 +101,12 @@ class FilamentParameters(BaseModel):
|
||||
),
|
||||
examples=["FF0000,00FF00,0000FF"],
|
||||
)
|
||||
multi_color_direction: Optional[MultiColorDirection] = Field(
|
||||
multi_color_direction: MultiColorDirection | None = Field(
|
||||
None,
|
||||
description=("Type of multi-color filament. Only set if the color_hex field contains multiple colors. "),
|
||||
examples=["coaxial", "longitudinal"],
|
||||
)
|
||||
external_id: Optional[str] = Field(
|
||||
external_id: str | None = Field(
|
||||
None,
|
||||
max_length=256,
|
||||
description=(
|
||||
@@ -114,14 +114,14 @@ class FilamentParameters(BaseModel):
|
||||
),
|
||||
examples=["polymaker_pla_polysonicblack_1000_175"],
|
||||
)
|
||||
extra: Optional[dict[str, str]] = Field(
|
||||
extra: dict[str, str] | None = Field(
|
||||
None,
|
||||
description="Extra fields for this filament.",
|
||||
)
|
||||
|
||||
@field_validator("color_hex")
|
||||
@classmethod
|
||||
def color_hex_validator(cls, v: Optional[str]) -> Optional[str]:
|
||||
def color_hex_validator(cls, v: str | None) -> str | None:
|
||||
"""Validate the color_hex field."""
|
||||
if not v:
|
||||
return None
|
||||
@@ -140,7 +140,7 @@ class FilamentParameters(BaseModel):
|
||||
|
||||
@field_validator("multi_color_hexes")
|
||||
@classmethod
|
||||
def multi_color_hexes_validator(cls, v: Optional[str]) -> Optional[str]:
|
||||
def multi_color_hexes_validator(cls, v: str | None) -> str | None:
|
||||
"""Validate the multi_color_hexes field."""
|
||||
if not v:
|
||||
return None
|
||||
@@ -173,12 +173,12 @@ class FilamentParameters(BaseModel):
|
||||
|
||||
|
||||
class FilamentUpdateParameters(FilamentParameters):
|
||||
density: Optional[float] = Field(None, gt=0, description="The density of this filament in g/cm3.", examples=[1.24])
|
||||
diameter: Optional[float] = Field(None, gt=0, description="The diameter of this filament in mm.", examples=[1.75])
|
||||
density: float | None = Field(None, gt=0, description="The density of this filament in g/cm3.", examples=[1.24])
|
||||
diameter: float | None = Field(None, gt=0, description="The diameter of this filament in mm.", examples=[1.75])
|
||||
|
||||
@field_validator("density", "diameter")
|
||||
@classmethod
|
||||
def prevent_none(cls: type["FilamentUpdateParameters"], v: Optional[float]) -> Optional[float]:
|
||||
def prevent_none(cls: type["FilamentUpdateParameters"], v: float | None) -> float | None:
|
||||
"""Prevent density and diameter from being None."""
|
||||
if v is None:
|
||||
raise ValueError("Value must not be None.")
|
||||
@@ -203,11 +203,11 @@ async def find(
|
||||
*,
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
vendor_name_old: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(alias="vendor_name", title="Vendor Name", description="See vendor.name.", deprecated=True),
|
||||
] = None,
|
||||
vendor_id_old: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
alias="vendor_id",
|
||||
title="Vendor ID",
|
||||
@@ -217,7 +217,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
vendor_name: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
alias="vendor.name",
|
||||
title="Vendor Name",
|
||||
@@ -229,7 +229,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
vendor_id: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
alias="vendor.id",
|
||||
title="Vendor ID",
|
||||
@@ -242,7 +242,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
name: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
title="Filament Name",
|
||||
description=(
|
||||
@@ -253,7 +253,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
material: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
title="Filament Material",
|
||||
description=(
|
||||
@@ -264,7 +264,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
article_number: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
title="Filament Article Number",
|
||||
description=(
|
||||
@@ -276,7 +276,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
color_hex: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
title="Filament Color",
|
||||
description="Match filament by similar color. Slow operation!",
|
||||
@@ -293,7 +293,7 @@ async def find(
|
||||
),
|
||||
] = 20.0,
|
||||
external_id: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
description=(
|
||||
"Find filaments imported by the given external ID. "
|
||||
@@ -305,7 +305,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
sort: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
title="Sort",
|
||||
description=(
|
||||
@@ -315,7 +315,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
limit: Annotated[
|
||||
Optional[int],
|
||||
int | None,
|
||||
Query(title="Limit", description="Maximum number of items in the response."),
|
||||
] = None,
|
||||
offset: Annotated[int, Query(title="Offset", description="Offset in the full result set if a limit is set.")] = 0,
|
||||
|
||||
+33
-33
@@ -2,7 +2,7 @@
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import Annotated, Literal, Optional
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from pydantic import BaseModel, Field, PlainSerializer
|
||||
|
||||
@@ -36,7 +36,7 @@ class SettingKV(BaseModel):
|
||||
setting: SettingResponse = Field(description="Setting value.")
|
||||
|
||||
@staticmethod
|
||||
def from_db(definition: SettingDefinition, set_value: Optional[str]) -> "SettingKV":
|
||||
def from_db(definition: SettingDefinition, set_value: str | None) -> "SettingKV":
|
||||
"""Create a new Pydantic vendor object from a database vendor object."""
|
||||
return SettingKV(
|
||||
key=definition.key,
|
||||
@@ -52,19 +52,19 @@ class Vendor(BaseModel):
|
||||
id: int = Field(description="Unique internal ID of this vendor.")
|
||||
registered: SpoolmanDateTime = Field(description="When the vendor was registered in the database. UTC Timezone.")
|
||||
name: str = Field(max_length=64, description="Vendor name.", examples=["Polymaker"])
|
||||
comment: Optional[str] = Field(
|
||||
comment: str | None = Field(
|
||||
None,
|
||||
max_length=1024,
|
||||
description="Free text comment about this vendor.",
|
||||
examples=[""],
|
||||
)
|
||||
empty_spool_weight: Optional[float] = Field(
|
||||
empty_spool_weight: float | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description="The empty spool weight, in grams.",
|
||||
examples=[140],
|
||||
)
|
||||
external_id: Optional[str] = Field(
|
||||
external_id: str | None = Field(
|
||||
None,
|
||||
max_length=256,
|
||||
description=(
|
||||
@@ -103,7 +103,7 @@ class MultiColorDirection(Enum):
|
||||
class Filament(BaseModel):
|
||||
id: int = Field(description="Unique internal ID of this filament type.")
|
||||
registered: SpoolmanDateTime = Field(description="When the filament was registered in the database. UTC Timezone.")
|
||||
name: Optional[str] = Field(
|
||||
name: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description=(
|
||||
@@ -112,14 +112,14 @@ class Filament(BaseModel):
|
||||
),
|
||||
examples=["PolyTerra™ Charcoal Black"],
|
||||
)
|
||||
vendor: Optional[Vendor] = Field(None, description="The vendor of this filament type.")
|
||||
material: Optional[str] = Field(
|
||||
vendor: Vendor | None = Field(None, description="The vendor of this filament type.")
|
||||
material: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="The material of this filament, e.g. PLA.",
|
||||
examples=["PLA"],
|
||||
)
|
||||
price: Optional[float] = Field(
|
||||
price: float | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description="The price of this filament in the system configured currency.",
|
||||
@@ -127,38 +127,38 @@ class Filament(BaseModel):
|
||||
)
|
||||
density: float = Field(gt=0, description="The density of this filament in g/cm3.", examples=[1.24])
|
||||
diameter: float = Field(gt=0, description="The diameter of this filament in mm.", examples=[1.75])
|
||||
weight: Optional[float] = Field(
|
||||
weight: float | None = Field(
|
||||
None,
|
||||
gt=0,
|
||||
description="The weight of the filament in a full spool, in grams.",
|
||||
examples=[1000],
|
||||
)
|
||||
spool_weight: Optional[float] = Field(None, ge=0, description="The empty spool weight, in grams.", examples=[140])
|
||||
article_number: Optional[str] = Field(
|
||||
spool_weight: float | None = Field(None, ge=0, description="The empty spool weight, in grams.", examples=[140])
|
||||
article_number: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Vendor article number, e.g. EAN, QR code, etc.",
|
||||
examples=["PM70820"],
|
||||
)
|
||||
comment: Optional[str] = Field(
|
||||
comment: str | None = Field(
|
||||
None,
|
||||
max_length=1024,
|
||||
description="Free text comment about this filament type.",
|
||||
examples=[""],
|
||||
)
|
||||
settings_extruder_temp: Optional[int] = Field(
|
||||
settings_extruder_temp: int | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description="Overridden extruder temperature, in °C.",
|
||||
examples=[210],
|
||||
)
|
||||
settings_bed_temp: Optional[int] = Field(
|
||||
settings_bed_temp: int | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description="Overridden bed temperature, in °C.",
|
||||
examples=[60],
|
||||
)
|
||||
color_hex: Optional[str] = Field(
|
||||
color_hex: str | None = Field(
|
||||
None,
|
||||
min_length=6,
|
||||
max_length=8,
|
||||
@@ -168,7 +168,7 @@ class Filament(BaseModel):
|
||||
),
|
||||
examples=["FF0000"],
|
||||
)
|
||||
multi_color_hexes: Optional[str] = Field(
|
||||
multi_color_hexes: str | None = Field(
|
||||
None,
|
||||
min_length=6,
|
||||
description=(
|
||||
@@ -178,12 +178,12 @@ class Filament(BaseModel):
|
||||
),
|
||||
examples=["FF0000,00FF00,0000FF"],
|
||||
)
|
||||
multi_color_direction: Optional[MultiColorDirection] = Field(
|
||||
multi_color_direction: MultiColorDirection | None = Field(
|
||||
None,
|
||||
description=("Type of multi-color filament. Only set if the multi_color_hexes field is set."),
|
||||
examples=["coaxial", "longitudinal"],
|
||||
)
|
||||
external_id: Optional[str] = Field(
|
||||
external_id: str | None = Field(
|
||||
None,
|
||||
max_length=256,
|
||||
description=(
|
||||
@@ -229,22 +229,22 @@ class Filament(BaseModel):
|
||||
class Spool(BaseModel):
|
||||
id: int = Field(description="Unique internal ID of this spool of filament.")
|
||||
registered: SpoolmanDateTime = Field(description="When the spool was registered in the database. UTC Timezone.")
|
||||
first_used: Optional[SpoolmanDateTime] = Field(
|
||||
first_used: SpoolmanDateTime | None = Field(
|
||||
None,
|
||||
description="First logged occurence of spool usage. UTC Timezone.",
|
||||
)
|
||||
last_used: Optional[SpoolmanDateTime] = Field(
|
||||
last_used: SpoolmanDateTime | None = Field(
|
||||
None,
|
||||
description="Last logged occurence of spool usage. UTC Timezone.",
|
||||
)
|
||||
filament: Filament = Field(description="The filament type of this spool.")
|
||||
price: Optional[float] = Field(
|
||||
price: float | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description="The price of this spool in the system configured currency.",
|
||||
examples=[20.0],
|
||||
)
|
||||
remaining_weight: Optional[float] = Field(
|
||||
remaining_weight: float | None = Field(
|
||||
default=None,
|
||||
ge=0,
|
||||
description=(
|
||||
@@ -253,13 +253,13 @@ class Spool(BaseModel):
|
||||
),
|
||||
examples=[500.6],
|
||||
)
|
||||
initial_weight: Optional[float] = Field(
|
||||
initial_weight: float | None = Field(
|
||||
default=None,
|
||||
ge=0,
|
||||
description=("The initial weight, in grams, of the filament on the spool (net weight)."),
|
||||
examples=[1246],
|
||||
)
|
||||
spool_weight: Optional[float] = Field(
|
||||
spool_weight: float | None = Field(
|
||||
default=None,
|
||||
ge=0,
|
||||
description=("Weight of an empty spool (tare weight)."),
|
||||
@@ -270,7 +270,7 @@ class Spool(BaseModel):
|
||||
description="Consumed weight of filament from the spool in grams.",
|
||||
examples=[500.3],
|
||||
)
|
||||
remaining_length: Optional[float] = Field(
|
||||
remaining_length: float | None = Field(
|
||||
default=None,
|
||||
ge=0,
|
||||
description=(
|
||||
@@ -284,19 +284,19 @@ class Spool(BaseModel):
|
||||
description="Consumed length of filament from the spool in millimeters.",
|
||||
examples=[50.7],
|
||||
)
|
||||
location: Optional[str] = Field(
|
||||
location: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Where this spool can be found.",
|
||||
examples=["Shelf A"],
|
||||
)
|
||||
lot_nr: Optional[str] = Field(
|
||||
lot_nr: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Vendor manufacturing lot/batch number of the spool.",
|
||||
examples=["52342"],
|
||||
)
|
||||
comment: Optional[str] = Field(
|
||||
comment: str | None = Field(
|
||||
None,
|
||||
max_length=1024,
|
||||
description="Free text comment about this specific spool.",
|
||||
@@ -315,8 +315,8 @@ class Spool(BaseModel):
|
||||
"""Create a new Pydantic spool object from a database spool object."""
|
||||
filament = Filament.from_db(item.filament)
|
||||
|
||||
remaining_weight: Optional[float] = None
|
||||
remaining_length: Optional[float] = None
|
||||
remaining_weight: float | None = None
|
||||
remaining_length: float | None = None
|
||||
|
||||
if item.initial_weight is not None:
|
||||
remaining_weight = max(item.initial_weight - item.used_weight, 0)
|
||||
@@ -368,8 +368,8 @@ class Info(BaseModel):
|
||||
logs_dir: str = Field(examples=["/home/app/.local/share/spoolman"])
|
||||
backups_dir: str = Field(examples=["/home/app/.local/share/spoolman/backups"])
|
||||
db_type: str = Field(examples=["sqlite"])
|
||||
git_commit: Optional[str] = Field(None, examples=["a1b2c3d"])
|
||||
build_date: Optional[SpoolmanDateTime] = Field(None, examples=["2021-01-01T00:00:00Z"])
|
||||
git_commit: str | None = Field(None, examples=["a1b2c3d"])
|
||||
build_date: SpoolmanDateTime | None = Field(None, examples=["2021-01-01T00:00:00Z"])
|
||||
|
||||
|
||||
class HealthCheck(BaseModel):
|
||||
|
||||
@@ -17,7 +17,7 @@ router = APIRouter(
|
||||
tags=["other"],
|
||||
)
|
||||
|
||||
# ruff: noqa: D103, B008
|
||||
# ruff: noqa: D103
|
||||
|
||||
|
||||
@router.get(
|
||||
|
||||
@@ -36,7 +36,7 @@ app = FastAPI(
|
||||
|
||||
@app.exception_handler(ItemNotFoundError)
|
||||
async def itemnotfounderror_exception_handler(_request: Request, exc: ItemNotFoundError) -> Response:
|
||||
logger.debug(exc, exc_info=True)
|
||||
logger.debug(exc)
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"message": exc.args[0]},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Annotated, Union
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import JSONResponse
|
||||
@@ -20,7 +20,7 @@ router = APIRouter(
|
||||
tags=["setting"],
|
||||
)
|
||||
|
||||
# ruff: noqa: D103,B008
|
||||
# ruff: noqa: D103
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -58,7 +58,7 @@ async def notify_any(
|
||||
async def get(
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
key: str,
|
||||
) -> Union[SettingResponse, JSONResponse]:
|
||||
) -> SettingResponse | JSONResponse:
|
||||
try:
|
||||
definition = parse_setting(key)
|
||||
except ValueError as e:
|
||||
@@ -88,7 +88,7 @@ async def get(
|
||||
)
|
||||
async def find(
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
) -> Union[dict[str, SettingResponse], JSONResponse]:
|
||||
) -> dict[str, SettingResponse] | JSONResponse:
|
||||
settings: dict[str, SettingResponse] = {}
|
||||
|
||||
# First get all settings that have been set.
|
||||
@@ -158,7 +158,7 @@ async def update(
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
key: str,
|
||||
body: Annotated[str, Body()],
|
||||
) -> Union[SettingResponse, JSONResponse]:
|
||||
) -> SettingResponse | JSONResponse:
|
||||
try:
|
||||
definition = parse_setting(key)
|
||||
except ValueError as e:
|
||||
|
||||
+31
-31
@@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Annotated, Optional
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, WebSocket, WebSocketDisconnect
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
@@ -26,32 +26,32 @@ router = APIRouter(
|
||||
tags=["spool"],
|
||||
)
|
||||
|
||||
# ruff: noqa: D103,B008
|
||||
# ruff: noqa: D103
|
||||
|
||||
|
||||
class SpoolParameters(BaseModel):
|
||||
first_used: Optional[datetime] = Field(None, description="First logged occurence of spool usage.")
|
||||
last_used: Optional[datetime] = Field(None, description="Last logged occurence of spool usage.")
|
||||
first_used: datetime | None = Field(None, description="First logged occurence of spool usage.")
|
||||
last_used: datetime | None = Field(None, description="Last logged occurence of spool usage.")
|
||||
filament_id: int = Field(description="The ID of the filament type of this spool.")
|
||||
price: Optional[float] = Field(
|
||||
price: float | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description="The price of this filament in the system configured currency.",
|
||||
examples=[20.0],
|
||||
)
|
||||
initial_weight: Optional[float] = Field(
|
||||
initial_weight: float | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description="The initial weight of the filament on the spool, in grams. (net weight)",
|
||||
examples=[200],
|
||||
)
|
||||
spool_weight: Optional[float] = Field(
|
||||
spool_weight: float | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description="The weight of an empty spool, in grams. (tare weight)",
|
||||
examples=[200],
|
||||
)
|
||||
remaining_weight: Optional[float] = Field(
|
||||
remaining_weight: float | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description=(
|
||||
@@ -59,43 +59,43 @@ class SpoolParameters(BaseModel):
|
||||
),
|
||||
examples=[800],
|
||||
)
|
||||
used_weight: Optional[float] = Field(
|
||||
used_weight: float | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description="Used weight of filament on the spool.",
|
||||
examples=[200],
|
||||
)
|
||||
location: Optional[str] = Field(
|
||||
location: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Where this spool can be found.",
|
||||
examples=["Shelf A"],
|
||||
)
|
||||
lot_nr: Optional[str] = Field(
|
||||
lot_nr: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Vendor manufacturing lot/batch number of the spool.",
|
||||
examples=["52342"],
|
||||
)
|
||||
comment: Optional[str] = Field(
|
||||
comment: str | None = Field(
|
||||
None,
|
||||
max_length=1024,
|
||||
description="Free text comment about this specific spool.",
|
||||
examples=[""],
|
||||
)
|
||||
archived: bool = Field(default=False, description="Whether this spool is archived and should not be used anymore.")
|
||||
extra: Optional[dict[str, str]] = Field(
|
||||
extra: dict[str, str] | None = Field(
|
||||
None,
|
||||
description="Extra fields for this spool.",
|
||||
)
|
||||
|
||||
|
||||
class SpoolUpdateParameters(SpoolParameters):
|
||||
filament_id: Optional[int] = Field(None, description="The ID of the filament type of this spool.")
|
||||
filament_id: int | None = Field(None, description="The ID of the filament type of this spool.")
|
||||
|
||||
@field_validator("filament_id")
|
||||
@classmethod
|
||||
def prevent_none(cls: type["SpoolUpdateParameters"], v: Optional[int]) -> Optional[int]:
|
||||
def prevent_none(cls: type["SpoolUpdateParameters"], v: int | None) -> int | None:
|
||||
"""Prevent filament_id from being None."""
|
||||
if v is None:
|
||||
raise ValueError("Value must not be None.")
|
||||
@@ -103,8 +103,8 @@ class SpoolUpdateParameters(SpoolParameters):
|
||||
|
||||
|
||||
class SpoolUseParameters(BaseModel):
|
||||
use_length: Optional[float] = Field(None, description="Length of filament to reduce by, in mm.", examples=[2.2])
|
||||
use_weight: Optional[float] = Field(None, description="Filament weight to reduce by, in g.", examples=[5.3])
|
||||
use_length: float | None = Field(None, description="Length of filament to reduce by, in mm.", examples=[2.2])
|
||||
use_weight: float | None = Field(None, description="Filament weight to reduce by, in g.", examples=[5.3])
|
||||
|
||||
|
||||
class SpoolMeasureParameters(BaseModel):
|
||||
@@ -129,11 +129,11 @@ async def find(
|
||||
*,
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
filament_name_old: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(alias="filament_name", title="Filament Name", description="See filament.name.", deprecated=True),
|
||||
] = None,
|
||||
filament_id_old: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
alias="filament_id",
|
||||
title="Filament ID",
|
||||
@@ -143,7 +143,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
filament_material_old: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
alias="filament_material",
|
||||
title="Filament Material",
|
||||
@@ -152,11 +152,11 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
vendor_name_old: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(alias="vendor_name", title="Vendor Name", description="See filament.vendor.name.", deprecated=True),
|
||||
] = None,
|
||||
vendor_id_old: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
alias="vendor_id",
|
||||
title="Vendor ID",
|
||||
@@ -166,7 +166,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
filament_name: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
alias="filament.name",
|
||||
title="Filament Name",
|
||||
@@ -178,7 +178,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
filament_id: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
alias="filament.id",
|
||||
title="Filament ID",
|
||||
@@ -188,7 +188,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
filament_material: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
alias="filament.material",
|
||||
title="Filament Material",
|
||||
@@ -200,7 +200,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
filament_vendor_name: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
alias="filament.vendor.name",
|
||||
title="Vendor Name",
|
||||
@@ -213,7 +213,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
filament_vendor_id: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
alias="filament.vendor.id",
|
||||
title="Vendor ID",
|
||||
@@ -226,7 +226,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
location: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
title="Location",
|
||||
description=(
|
||||
@@ -237,7 +237,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
lot_nr: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
title="Lot/Batch Number",
|
||||
description=(
|
||||
@@ -252,7 +252,7 @@ async def find(
|
||||
Query(title="Allow Archived", description="Whether to include archived spools in the search results."),
|
||||
] = False,
|
||||
sort: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
title="Sort",
|
||||
description=(
|
||||
@@ -262,7 +262,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
limit: Annotated[
|
||||
Optional[int],
|
||||
int | None,
|
||||
Query(title="Limit", description="Maximum number of items in the response."),
|
||||
] = None,
|
||||
offset: Annotated[int, Query(title="Offset", description="Offset in the full result set if a limit is set.")] = 0,
|
||||
|
||||
+12
-12
@@ -1,7 +1,7 @@
|
||||
"""Vendor related endpoints."""
|
||||
|
||||
import asyncio
|
||||
from typing import Annotated, Optional
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, WebSocket, WebSocketDisconnect
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
@@ -21,24 +21,24 @@ router = APIRouter(
|
||||
tags=["vendor"],
|
||||
)
|
||||
|
||||
# ruff: noqa: D103,B008
|
||||
# ruff: noqa: D103
|
||||
|
||||
|
||||
class VendorParameters(BaseModel):
|
||||
name: str = Field(max_length=64, description="Vendor name.", examples=["Polymaker"])
|
||||
comment: Optional[str] = Field(
|
||||
comment: str | None = Field(
|
||||
None,
|
||||
max_length=1024,
|
||||
description="Free text comment about this vendor.",
|
||||
examples=[""],
|
||||
)
|
||||
empty_spool_weight: Optional[float] = Field(
|
||||
empty_spool_weight: float | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description="The weight of an empty spool, in grams.",
|
||||
examples=[200],
|
||||
)
|
||||
external_id: Optional[str] = Field(
|
||||
external_id: str | None = Field(
|
||||
None,
|
||||
max_length=256,
|
||||
description=(
|
||||
@@ -46,18 +46,18 @@ class VendorParameters(BaseModel):
|
||||
),
|
||||
examples=["eSun"],
|
||||
)
|
||||
extra: Optional[dict[str, str]] = Field(
|
||||
extra: dict[str, str] | None = Field(
|
||||
None,
|
||||
description="Extra fields for this vendor.",
|
||||
)
|
||||
|
||||
|
||||
class VendorUpdateParameters(VendorParameters):
|
||||
name: Optional[str] = Field(None, max_length=64, description="Vendor name.", examples=["Polymaker"])
|
||||
name: str | None = Field(None, max_length=64, description="Vendor name.", examples=["Polymaker"])
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def prevent_none(cls: type["VendorUpdateParameters"], v: Optional[str]) -> Optional[str]:
|
||||
def prevent_none(cls: type["VendorUpdateParameters"], v: str | None) -> str | None:
|
||||
"""Prevent name from being None."""
|
||||
if v is None:
|
||||
raise ValueError("Value must not be None.")
|
||||
@@ -81,7 +81,7 @@ class VendorUpdateParameters(VendorParameters):
|
||||
async def find(
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
name: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
title="Vendor Name",
|
||||
description=(
|
||||
@@ -91,7 +91,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
external_id: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
title="Vendor External ID",
|
||||
description=(
|
||||
@@ -103,7 +103,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
sort: Annotated[
|
||||
Optional[str],
|
||||
str | None,
|
||||
Query(
|
||||
title="Sort",
|
||||
description=(
|
||||
@@ -113,7 +113,7 @@ async def find(
|
||||
),
|
||||
] = None,
|
||||
limit: Annotated[
|
||||
Optional[int],
|
||||
int | None,
|
||||
Query(title="Limit", description="Maximum number of items in the response."),
|
||||
] = None,
|
||||
offset: Annotated[int, Query(title="Offset", description="Offset in the full result set if a limit is set.")] = 0,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
"""A python script that bumps the version number of a project."""
|
||||
|
||||
# ruff: noqa: PLR2004, T201, S603, S607
|
||||
# ruff: noqa: PLR2004, T201, S607
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
+1
-3
@@ -1,7 +1,5 @@
|
||||
"""Functions for providing the client interface."""
|
||||
|
||||
# ruff: noqa: PTH118
|
||||
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import MutableMapping
|
||||
@@ -66,7 +64,7 @@ class SinglePageApplication(StaticFiles):
|
||||
return NotModifiedResponse(response.headers)
|
||||
return response
|
||||
|
||||
def lookup_path(self, path: str) -> tuple[str, Union[os.stat_result, None]]:
|
||||
def lookup_path(self, path: str) -> tuple[str, os.stat_result | None]:
|
||||
"""Return index.html if the requested file cannot be found."""
|
||||
path = path.removeprefix(self.base_path).removeprefix("/")
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import sqlite3
|
||||
from collections.abc import AsyncGenerator
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
|
||||
from scheduler.asyncio.scheduler import Scheduler
|
||||
from sqlalchemy import URL
|
||||
@@ -56,8 +55,8 @@ def get_connection_url() -> URL:
|
||||
|
||||
class Database:
|
||||
connection_url: URL
|
||||
engine: Optional[AsyncEngine]
|
||||
session_maker: Optional[async_sessionmaker[AsyncSession]]
|
||||
engine: AsyncEngine | None
|
||||
session_maker: async_sessionmaker[AsyncSession] | None
|
||||
|
||||
def __init__(self, connection_url: URL) -> None:
|
||||
"""Construct the Database wrapper and set config parameters."""
|
||||
@@ -90,7 +89,7 @@ class Database:
|
||||
)
|
||||
self.session_maker = async_sessionmaker(self.engine, autocommit=False, autoflush=True, expire_on_commit=False)
|
||||
|
||||
def backup(self, target_path: Union[str, PathLike[str]]) -> None:
|
||||
def backup(self, target_path: str | PathLike[str]) -> None:
|
||||
"""Backup the database."""
|
||||
if not self.is_file_based_sqlite() or self.connection_url.database is None:
|
||||
return
|
||||
@@ -112,9 +111,9 @@ class Database:
|
||||
|
||||
def backup_and_rotate(
|
||||
self,
|
||||
backup_folder: Union[str, PathLike[str]],
|
||||
backup_folder: str | PathLike[str],
|
||||
num_backups: int = 5,
|
||||
) -> Optional[Path]:
|
||||
) -> Path | None:
|
||||
"""Backup the database and rotate existing backups.
|
||||
|
||||
Args:
|
||||
@@ -155,7 +154,7 @@ class Database:
|
||||
return backup_path
|
||||
|
||||
|
||||
__db: Optional[Database] = None
|
||||
__db: Database | None = None
|
||||
|
||||
|
||||
def setup_db(connection_url: URL) -> None:
|
||||
@@ -170,7 +169,7 @@ def setup_db(connection_url: URL) -> None:
|
||||
__db.connect()
|
||||
|
||||
|
||||
async def backup_global_db(num_backups: int = 5) -> Optional[Path]:
|
||||
async def backup_global_db(num_backups: int = 5) -> Path | None:
|
||||
"""Backup the database and rotate existing backups.
|
||||
|
||||
Returns:
|
||||
@@ -182,7 +181,7 @@ async def backup_global_db(num_backups: int = 5) -> Optional[Path]:
|
||||
return __db.backup_and_rotate(env.get_backups_dir(), num_backups=num_backups)
|
||||
|
||||
|
||||
async def _backup_task() -> Optional[Path]:
|
||||
async def _backup_task() -> Path | None:
|
||||
"""Perform scheduled backup of the database."""
|
||||
logger.info("Performing scheduled database backup.")
|
||||
if __db is None:
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from datetime import datetime
|
||||
from typing import Optional, Union
|
||||
|
||||
import sqlalchemy
|
||||
from sqlalchemy import func, select
|
||||
@@ -31,24 +30,24 @@ async def create(
|
||||
db: AsyncSession,
|
||||
density: float,
|
||||
diameter: float,
|
||||
name: Optional[str] = None,
|
||||
vendor_id: Optional[int] = None,
|
||||
material: Optional[str] = None,
|
||||
price: Optional[float] = None,
|
||||
weight: Optional[float] = None,
|
||||
spool_weight: Optional[float] = None,
|
||||
article_number: Optional[str] = None,
|
||||
comment: Optional[str] = None,
|
||||
settings_extruder_temp: Optional[int] = None,
|
||||
settings_bed_temp: Optional[int] = None,
|
||||
color_hex: Optional[str] = None,
|
||||
multi_color_hexes: Optional[str] = None,
|
||||
multi_color_direction: Optional[MultiColorDirection] = None,
|
||||
external_id: Optional[str] = None,
|
||||
extra: Optional[dict[str, str]] = None,
|
||||
name: str | None = None,
|
||||
vendor_id: int | None = None,
|
||||
material: str | None = None,
|
||||
price: float | None = None,
|
||||
weight: float | None = None,
|
||||
spool_weight: float | None = None,
|
||||
article_number: str | None = None,
|
||||
comment: str | None = None,
|
||||
settings_extruder_temp: int | None = None,
|
||||
settings_bed_temp: int | None = None,
|
||||
color_hex: str | None = None,
|
||||
multi_color_hexes: str | None = None,
|
||||
multi_color_direction: MultiColorDirection | None = None,
|
||||
external_id: str | None = None,
|
||||
extra: dict[str, str] | None = None,
|
||||
) -> models.Filament:
|
||||
"""Add a new filament to the database."""
|
||||
vendor_item: Optional[models.Vendor] = None
|
||||
vendor_item: models.Vendor | None = None
|
||||
if vendor_id is not None:
|
||||
vendor_item = await vendor.get_by_id(db, vendor_id)
|
||||
# default spool weight from vendor
|
||||
@@ -96,15 +95,15 @@ async def get_by_id(db: AsyncSession, filament_id: int) -> models.Filament:
|
||||
async def find(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
ids: Optional[list[int]] = None,
|
||||
vendor_name: Optional[str] = None,
|
||||
vendor_id: Optional[Union[int, Sequence[int]]] = None,
|
||||
name: Optional[str] = None,
|
||||
material: Optional[str] = None,
|
||||
article_number: Optional[str] = None,
|
||||
external_id: Optional[str] = None,
|
||||
sort_by: Optional[dict[str, SortOrder]] = None,
|
||||
limit: Optional[int] = None,
|
||||
ids: list[int] | None = None,
|
||||
vendor_name: str | None = None,
|
||||
vendor_id: int | Sequence[int] | None = None,
|
||||
name: str | None = None,
|
||||
material: str | None = None,
|
||||
article_number: str | None = None,
|
||||
external_id: str | None = None,
|
||||
sort_by: dict[str, SortOrder] | None = None,
|
||||
limit: int | None = None,
|
||||
offset: int = 0,
|
||||
) -> tuple[list[models.Filament], int]:
|
||||
"""Find a list of filament objects by search criteria.
|
||||
|
||||
+26
-26
@@ -18,10 +18,10 @@ class Vendor(Base):
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
registered: Mapped[datetime] = mapped_column()
|
||||
name: Mapped[str] = mapped_column(String(64))
|
||||
empty_spool_weight: Mapped[Optional[float]] = mapped_column(comment="The weight of an empty spool.")
|
||||
comment: Mapped[Optional[str]] = mapped_column(String(1024))
|
||||
empty_spool_weight: Mapped[float | None] = mapped_column(comment="The weight of an empty spool.")
|
||||
comment: Mapped[str | None] = mapped_column(String(1024))
|
||||
filaments: Mapped[list["Filament"]] = relationship(back_populates="vendor")
|
||||
external_id: Mapped[Optional[str]] = mapped_column(String(256))
|
||||
external_id: Mapped[str | None] = mapped_column(String(256))
|
||||
extra: Mapped[list["VendorField"]] = relationship(
|
||||
back_populates="vendor",
|
||||
cascade="save-update, merge, delete, delete-orphan",
|
||||
@@ -34,24 +34,24 @@ class Filament(Base):
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
||||
registered: Mapped[datetime] = mapped_column()
|
||||
name: Mapped[Optional[str]] = mapped_column(String(64))
|
||||
vendor_id: Mapped[Optional[int]] = mapped_column(ForeignKey("vendor.id"))
|
||||
name: Mapped[str | None] = mapped_column(String(64))
|
||||
vendor_id: Mapped[int | None] = mapped_column(ForeignKey("vendor.id"))
|
||||
vendor: Mapped[Optional["Vendor"]] = relationship(back_populates="filaments")
|
||||
spools: Mapped[list["Spool"]] = relationship(back_populates="filament")
|
||||
material: Mapped[Optional[str]] = mapped_column(String(64))
|
||||
price: Mapped[Optional[float]] = mapped_column()
|
||||
material: Mapped[str | None] = mapped_column(String(64))
|
||||
price: Mapped[float | None] = mapped_column()
|
||||
density: Mapped[float] = mapped_column()
|
||||
diameter: Mapped[float] = mapped_column()
|
||||
weight: Mapped[Optional[float]] = mapped_column(comment="The filament weight of a full spool (net weight).")
|
||||
spool_weight: Mapped[Optional[float]] = mapped_column(comment="The weight of an empty spool.")
|
||||
article_number: Mapped[Optional[str]] = mapped_column(String(64))
|
||||
comment: Mapped[Optional[str]] = mapped_column(String(1024))
|
||||
settings_extruder_temp: Mapped[Optional[int]] = mapped_column(comment="Overridden extruder temperature.")
|
||||
settings_bed_temp: Mapped[Optional[int]] = mapped_column(comment="Overridden bed temperature.")
|
||||
color_hex: Mapped[Optional[str]] = mapped_column(String(8))
|
||||
multi_color_hexes: Mapped[Optional[str]] = mapped_column(String(128))
|
||||
multi_color_direction: Mapped[Optional[str]] = mapped_column(String(16))
|
||||
external_id: Mapped[Optional[str]] = mapped_column(String(256))
|
||||
weight: Mapped[float | None] = mapped_column(comment="The filament weight of a full spool (net weight).")
|
||||
spool_weight: Mapped[float | None] = mapped_column(comment="The weight of an empty spool.")
|
||||
article_number: Mapped[str | None] = mapped_column(String(64))
|
||||
comment: Mapped[str | None] = mapped_column(String(1024))
|
||||
settings_extruder_temp: Mapped[int | None] = mapped_column(comment="Overridden extruder temperature.")
|
||||
settings_bed_temp: Mapped[int | None] = mapped_column(comment="Overridden bed temperature.")
|
||||
color_hex: Mapped[str | None] = mapped_column(String(8))
|
||||
multi_color_hexes: Mapped[str | None] = mapped_column(String(128))
|
||||
multi_color_direction: Mapped[str | None] = mapped_column(String(16))
|
||||
external_id: Mapped[str | None] = mapped_column(String(256))
|
||||
extra: Mapped[list["FilamentField"]] = relationship(
|
||||
back_populates="filament",
|
||||
cascade="save-update, merge, delete, delete-orphan",
|
||||
@@ -64,18 +64,18 @@ class Spool(Base):
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
||||
registered: Mapped[datetime] = mapped_column()
|
||||
first_used: Mapped[Optional[datetime]] = mapped_column()
|
||||
last_used: Mapped[Optional[datetime]] = mapped_column()
|
||||
price: Mapped[Optional[float]] = mapped_column()
|
||||
first_used: Mapped[datetime | None] = mapped_column()
|
||||
last_used: Mapped[datetime | None] = mapped_column()
|
||||
price: Mapped[float | None] = mapped_column()
|
||||
filament_id: Mapped[int] = mapped_column(ForeignKey("filament.id"))
|
||||
filament: Mapped["Filament"] = relationship(back_populates="spools")
|
||||
initial_weight: Mapped[Optional[float]] = mapped_column()
|
||||
spool_weight: Mapped[Optional[float]] = mapped_column()
|
||||
initial_weight: Mapped[float | None] = mapped_column()
|
||||
spool_weight: Mapped[float | None] = mapped_column()
|
||||
used_weight: Mapped[float] = mapped_column()
|
||||
location: Mapped[Optional[str]] = mapped_column(String(64))
|
||||
lot_nr: Mapped[Optional[str]] = mapped_column(String(64))
|
||||
comment: Mapped[Optional[str]] = mapped_column(String(1024))
|
||||
archived: Mapped[Optional[bool]] = mapped_column()
|
||||
location: Mapped[str | None] = mapped_column(String(64))
|
||||
lot_nr: Mapped[str | None] = mapped_column(String(64))
|
||||
comment: Mapped[str | None] = mapped_column(String(1024))
|
||||
archived: Mapped[bool | None] = mapped_column()
|
||||
extra: Mapped[list["SpoolField"]] = relationship(
|
||||
back_populates="spool",
|
||||
cascade="save-update, merge, delete, delete-orphan",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Helper functions for interacting with vendor database objects."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -56,7 +55,7 @@ async def delete(db: AsyncSession, definition: SettingDefinition) -> None:
|
||||
await setting_changed(definition, None, EventType.DELETED)
|
||||
|
||||
|
||||
async def setting_changed(definition: SettingDefinition, set_value: Optional[str], typ: EventType) -> None:
|
||||
async def setting_changed(definition: SettingDefinition, set_value: str | None, typ: EventType) -> None:
|
||||
"""Notify websocket clients that a setting has changed."""
|
||||
await websocket_manager.send(
|
||||
("setting", str(definition.key)),
|
||||
|
||||
+20
-21
@@ -3,7 +3,6 @@
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, Union
|
||||
|
||||
import sqlalchemy
|
||||
from sqlalchemy import case, func
|
||||
@@ -38,18 +37,18 @@ async def create(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
filament_id: int,
|
||||
remaining_weight: Optional[float] = None,
|
||||
initial_weight: Optional[float] = None,
|
||||
spool_weight: Optional[float] = None,
|
||||
used_weight: Optional[float] = None,
|
||||
first_used: Optional[datetime] = None,
|
||||
last_used: Optional[datetime] = None,
|
||||
price: Optional[float] = None,
|
||||
location: Optional[str] = None,
|
||||
lot_nr: Optional[str] = None,
|
||||
comment: Optional[str] = None,
|
||||
remaining_weight: float | None = None,
|
||||
initial_weight: float | None = None,
|
||||
spool_weight: float | None = None,
|
||||
used_weight: float | None = None,
|
||||
first_used: datetime | None = None,
|
||||
last_used: datetime | None = None,
|
||||
price: float | None = None,
|
||||
location: str | None = None,
|
||||
lot_nr: str | None = None,
|
||||
comment: str | None = None,
|
||||
archived: bool = False,
|
||||
extra: Optional[dict[str, str]] = None,
|
||||
extra: dict[str, str] | None = None,
|
||||
) -> models.Spool:
|
||||
"""Add a new spool to the database. Leave weight empty to assume full spool."""
|
||||
filament_item = await filament.get_by_id(db, filament_id)
|
||||
@@ -115,16 +114,16 @@ async def get_by_id(db: AsyncSession, spool_id: int) -> models.Spool:
|
||||
async def find( # noqa: C901, PLR0912
|
||||
*,
|
||||
db: AsyncSession,
|
||||
filament_name: Optional[str] = None,
|
||||
filament_id: Optional[Union[int, Sequence[int]]] = None,
|
||||
filament_material: Optional[str] = None,
|
||||
vendor_name: Optional[str] = None,
|
||||
vendor_id: Optional[Union[int, Sequence[int]]] = None,
|
||||
location: Optional[str] = None,
|
||||
lot_nr: Optional[str] = None,
|
||||
filament_name: str | None = None,
|
||||
filament_id: int | Sequence[int] | None = None,
|
||||
filament_material: str | None = None,
|
||||
vendor_name: str | None = None,
|
||||
vendor_id: int | Sequence[int] | None = None,
|
||||
location: str | None = None,
|
||||
lot_nr: str | None = None,
|
||||
allow_archived: bool = False,
|
||||
sort_by: Optional[dict[str, SortOrder]] = None,
|
||||
limit: Optional[int] = None,
|
||||
sort_by: dict[str, SortOrder] | None = None,
|
||||
limit: int | None = None,
|
||||
offset: int = 0,
|
||||
) -> tuple[list[models.Spool], int]:
|
||||
"""Find a list of spool objects by search criteria.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Sequence
|
||||
from enum import Enum
|
||||
from typing import Any, Optional, TypeVar, Union
|
||||
from typing import Any, TypeVar
|
||||
|
||||
import sqlalchemy
|
||||
from sqlalchemy import Select
|
||||
@@ -40,8 +40,8 @@ def parse_nested_field(base_obj: type[models.Base], field: str) -> attributes.In
|
||||
|
||||
def add_where_clause_str_opt(
|
||||
stmt: Select,
|
||||
field: attributes.InstrumentedAttribute[Optional[str]],
|
||||
value: Optional[str],
|
||||
field: attributes.InstrumentedAttribute[str | None],
|
||||
value: str | None,
|
||||
) -> Select:
|
||||
"""Add a where clause to a select statement for an optional string field."""
|
||||
if value is not None:
|
||||
@@ -65,7 +65,7 @@ def add_where_clause_str_opt(
|
||||
def add_where_clause_str(
|
||||
stmt: Select,
|
||||
field: attributes.InstrumentedAttribute[str],
|
||||
value: Optional[str],
|
||||
value: str | None,
|
||||
) -> Select:
|
||||
"""Add a where clause to a select statement for a string field."""
|
||||
if value is not None:
|
||||
@@ -88,7 +88,7 @@ def add_where_clause_str(
|
||||
def add_where_clause_int(
|
||||
stmt: Select,
|
||||
field: attributes.InstrumentedAttribute[int],
|
||||
value: Optional[Union[int, Sequence[int]]],
|
||||
value: int | Sequence[int] | None,
|
||||
) -> Select:
|
||||
"""Add a where clause to a select statement for a field."""
|
||||
if value is not None:
|
||||
@@ -100,8 +100,8 @@ def add_where_clause_int(
|
||||
|
||||
def add_where_clause_int_opt(
|
||||
stmt: Select,
|
||||
field: attributes.InstrumentedAttribute[Optional[int]],
|
||||
value: Optional[Union[int, Sequence[int]]],
|
||||
field: attributes.InstrumentedAttribute[int | None],
|
||||
value: int | Sequence[int] | None,
|
||||
) -> Select:
|
||||
"""Add a where clause to a select statement for a field."""
|
||||
if value is not None:
|
||||
@@ -123,7 +123,7 @@ T = TypeVar("T")
|
||||
def add_where_clause_int_in(
|
||||
stmt: Select,
|
||||
field: attributes.InstrumentedAttribute[T],
|
||||
value: Optional[Sequence[T]],
|
||||
value: Sequence[T] | None,
|
||||
) -> Select:
|
||||
"""Add a where clause to a select statement for a field."""
|
||||
if value is not None:
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
import sqlalchemy
|
||||
from sqlalchemy import func, select
|
||||
@@ -20,11 +19,11 @@ logger = logging.getLogger(__name__)
|
||||
async def create(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
name: Optional[str] = None,
|
||||
comment: Optional[str] = None,
|
||||
empty_spool_weight: Optional[float] = None,
|
||||
external_id: Optional[str] = None,
|
||||
extra: Optional[dict[str, str]] = None,
|
||||
name: str | None = None,
|
||||
comment: str | None = None,
|
||||
empty_spool_weight: float | None = None,
|
||||
external_id: str | None = None,
|
||||
extra: dict[str, str] | None = None,
|
||||
) -> models.Vendor:
|
||||
"""Add a new vendor to the database."""
|
||||
vendor = models.Vendor(
|
||||
@@ -52,10 +51,10 @@ async def get_by_id(db: AsyncSession, vendor_id: int) -> models.Vendor:
|
||||
async def find(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
name: Optional[str] = None,
|
||||
external_id: Optional[str] = None,
|
||||
sort_by: Optional[dict[str, SortOrder]] = None,
|
||||
limit: Optional[int] = None,
|
||||
name: str | None = None,
|
||||
external_id: str | None = None,
|
||||
sort_by: dict[str, SortOrder] | None = None,
|
||||
limit: int | None = None,
|
||||
offset: int = 0,
|
||||
) -> tuple[list[models.Vendor], int]:
|
||||
"""Find a list of vendor objects by search criteria.
|
||||
|
||||
+12
-13
@@ -7,7 +7,6 @@ import sys
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from urllib import parse
|
||||
|
||||
from platformdirs import user_data_dir
|
||||
@@ -41,7 +40,7 @@ class DatabaseType(Enum):
|
||||
raise ValueError(f"Unknown database type '{self}'.")
|
||||
|
||||
|
||||
def get_database_type() -> Optional[DatabaseType]:
|
||||
def get_database_type() -> DatabaseType | None:
|
||||
"""Get the database type from environment variables.
|
||||
|
||||
Returns None if no environment variable was set for the database type.
|
||||
@@ -64,7 +63,7 @@ def get_database_type() -> Optional[DatabaseType]:
|
||||
raise ValueError(f"Failed to parse SPOOLMAN_DB_TYPE variable: Unknown database type '{database_type}'.")
|
||||
|
||||
|
||||
def get_host() -> Optional[str]:
|
||||
def get_host() -> str | None:
|
||||
"""Get the DB host from environment variables.
|
||||
|
||||
Returns None if no environment variable was set for the host.
|
||||
@@ -76,7 +75,7 @@ def get_host() -> Optional[str]:
|
||||
return os.getenv("SPOOLMAN_DB_HOST")
|
||||
|
||||
|
||||
def get_port() -> Optional[int]:
|
||||
def get_port() -> int | None:
|
||||
"""Get the DB port from environment variables.
|
||||
|
||||
Returns None if no environment variable was set for the port.
|
||||
@@ -94,7 +93,7 @@ def get_port() -> Optional[int]:
|
||||
raise ValueError(f"Failed to parse SPOOLMAN_DB_PORT variable: {exc!s}") from exc
|
||||
|
||||
|
||||
def get_database() -> Optional[str]:
|
||||
def get_database() -> str | None:
|
||||
"""Get the DB name from environment variables.
|
||||
|
||||
Returns None if no environment variable was set for the name.
|
||||
@@ -106,7 +105,7 @@ def get_database() -> Optional[str]:
|
||||
return os.getenv("SPOOLMAN_DB_NAME")
|
||||
|
||||
|
||||
def get_query() -> Optional[dict[str, str]]:
|
||||
def get_query() -> dict[str, str] | None:
|
||||
"""Get the DB query from environment variables.
|
||||
|
||||
Returns None if no environment variable was set for the query.
|
||||
@@ -125,7 +124,7 @@ def get_query() -> Optional[dict[str, str]]:
|
||||
raise ValueError(f"Failed to parse SPOOLMAN_DB_QUERY variable: {exc!s}") from exc
|
||||
|
||||
|
||||
def get_username() -> Optional[str]:
|
||||
def get_username() -> str | None:
|
||||
"""Get the DB username from environment variables.
|
||||
|
||||
Returns None if no environment variable was set for the username.
|
||||
@@ -137,7 +136,7 @@ def get_username() -> Optional[str]:
|
||||
return os.getenv("SPOOLMAN_DB_USERNAME")
|
||||
|
||||
|
||||
def get_password() -> Optional[str]:
|
||||
def get_password() -> str | None:
|
||||
"""Get the DB password from environment variables.
|
||||
|
||||
Returns None if no environment variables were set for the password.
|
||||
@@ -225,7 +224,7 @@ def is_cors_defined() -> bool:
|
||||
return cors not in {"FALSE", "0"}
|
||||
|
||||
|
||||
def get_cors_origin() -> Optional[list[str]]:
|
||||
def get_cors_origin() -> list[str] | None:
|
||||
"""Get the CORS origin from environment variables.
|
||||
|
||||
Returns None if no environment variable was set for the origin.
|
||||
@@ -327,7 +326,7 @@ def get_version() -> str:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def get_commit_hash() -> Optional[str]:
|
||||
def get_commit_hash() -> str | None:
|
||||
"""Get the latest commit hash of the package.
|
||||
|
||||
Can end with "-dirty" if there are uncommitted changes.
|
||||
@@ -348,7 +347,7 @@ def get_commit_hash() -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def get_build_date() -> Optional[datetime]:
|
||||
def get_build_date() -> datetime | None:
|
||||
"""Get the build date of the package.
|
||||
|
||||
Returns:
|
||||
@@ -389,7 +388,7 @@ def chown_dir(path: str) -> bool:
|
||||
try:
|
||||
uid = os.getuid()
|
||||
gid = os.getgid()
|
||||
subprocess.run(["chown", "-R", f"{uid}:{gid}", path], check=True) # noqa: S603, S607
|
||||
subprocess.run(["chown", "-R", f"{uid}:{gid}", path], check=True) # noqa: S607
|
||||
except: # noqa: E722
|
||||
return False
|
||||
return True
|
||||
@@ -428,7 +427,7 @@ def is_docker() -> bool:
|
||||
def is_data_dir_mounted() -> bool:
|
||||
"""Check if the data directory is mounted as a shfs."""
|
||||
# "mount" will give us a list of all mounted filesystems
|
||||
mounts = subprocess.run("mount", check=True, stdout=subprocess.PIPE, text=True) # noqa: S603, S607
|
||||
mounts = subprocess.run("mount", check=True, stdout=subprocess.PIPE, text=True) # noqa: S607
|
||||
data_dir = str(get_data_dir().resolve())
|
||||
return any(data_dir in line for line in mounts.stdout.splitlines())
|
||||
|
||||
|
||||
+11
-12
@@ -6,7 +6,6 @@ import os
|
||||
from collections.abc import Iterator
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import hishel
|
||||
@@ -63,28 +62,28 @@ class ExternalFilament(BaseModel):
|
||||
material: str = Field(description="Filament material.", examples=["PLA"])
|
||||
density: float = Field(description="Density in g/cm3.", examples=[1.23])
|
||||
weight: float = Field(description="Net weight of a single spool.", examples=[1000])
|
||||
spool_weight: Optional[float] = Field(default=None, description="Weight of an empty spool.", examples=[140])
|
||||
spool_type: Optional[SpoolType] = Field(None, description="Type of spool.", examples=[SpoolType.PLASTIC])
|
||||
spool_weight: float | None = Field(default=None, description="Weight of an empty spool.", examples=[140])
|
||||
spool_type: SpoolType | None = Field(None, description="Type of spool.", examples=[SpoolType.PLASTIC])
|
||||
diameter: float = Field(description="Filament in mm.", examples=[1.75])
|
||||
color_hex: Optional[str] = Field(
|
||||
color_hex: str | None = Field(
|
||||
default=None,
|
||||
description="Filament color code in hex format, for single-color filaments.",
|
||||
examples=["2c3232"],
|
||||
)
|
||||
color_hexes: Optional[list[str]] = Field(
|
||||
color_hexes: list[str] | None = Field(
|
||||
default=None,
|
||||
description="For multi-color filaments. List of hex color codes in hex format.",
|
||||
examples=[["2c3232", "5f5f5f"]],
|
||||
)
|
||||
extruder_temp: Optional[int] = Field(default=None, description="Extruder/nozzle temperature in °C.", examples=[210])
|
||||
bed_temp: Optional[int] = Field(default=None, description="Bed temperature in °C.", examples=[50])
|
||||
finish: Optional[Finish] = Field(default=None, description="Finish of the filament.", examples=[Finish.MATTE])
|
||||
multi_color_direction: Optional[MultiColorDirection] = Field(
|
||||
extruder_temp: int | None = Field(default=None, description="Extruder/nozzle temperature in °C.", examples=[210])
|
||||
bed_temp: int | None = Field(default=None, description="Bed temperature in °C.", examples=[50])
|
||||
finish: Finish | None = Field(default=None, description="Finish of the filament.", examples=[Finish.MATTE])
|
||||
multi_color_direction: MultiColorDirection | None = Field(
|
||||
default=None,
|
||||
description="Direction of multi-color filaments.",
|
||||
examples=[MultiColorDirection.COAXIAL],
|
||||
)
|
||||
pattern: Optional[Pattern] = Field(default=None, description="Pattern of the filament.", examples=[Pattern.MARBLE])
|
||||
pattern: Pattern | None = Field(default=None, description="Pattern of the filament.", examples=[Pattern.MARBLE])
|
||||
translucent: bool = Field(default=False, description="Whether the filament is translucent.")
|
||||
glow: bool = Field(default=False, description="Whether the filament is glow-in-the-dark.")
|
||||
|
||||
@@ -104,8 +103,8 @@ class ExternalFilamentsFile(RootModel):
|
||||
class ExternalMaterial(BaseModel):
|
||||
material: str = Field(examples=["PLA"])
|
||||
density: float = Field(examples=[1.24])
|
||||
extruder_temp: Optional[int] = Field(default=None, description="Extruder/nozzle temperature in °C.", examples=[210])
|
||||
bed_temp: Optional[int] = Field(default=None, description="Bed temperature in °C.", examples=[50])
|
||||
extruder_temp: int | None = Field(default=None, description="Extruder/nozzle temperature in °C.", examples=[210])
|
||||
bed_temp: int | None = Field(default=None, description="Bed temperature in °C.", examples=[50])
|
||||
|
||||
|
||||
class ExternalMaterialsFile(RootModel):
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import json
|
||||
import logging
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -39,15 +38,15 @@ class ExtraFieldType(Enum):
|
||||
class ExtraFieldParameters(BaseModel):
|
||||
name: str = Field(description="Nice name", min_length=1, max_length=128)
|
||||
order: int = Field(0, description="Order of the field")
|
||||
unit: Optional[str] = Field(None, description="Unit of the value", min_length=1, max_length=16)
|
||||
unit: str | None = Field(None, description="Unit of the value", min_length=1, max_length=16)
|
||||
field_type: ExtraFieldType = Field(description="Type of the field")
|
||||
default_value: Optional[str] = Field(None, description="Default value of the field")
|
||||
choices: Optional[list[str]] = Field(
|
||||
default_value: str | None = Field(None, description="Default value of the field")
|
||||
choices: list[str] | None = Field(
|
||||
None,
|
||||
description="Choices for the field, only for field type choice",
|
||||
min_length=1,
|
||||
)
|
||||
multi_choice: Optional[bool] = Field(None, description="Whether multiple choices can be selected")
|
||||
multi_choice: bool | None = Field(None, description="Whether multiple choices can be selected")
|
||||
|
||||
|
||||
class ExtraField(ExtraFieldParameters):
|
||||
|
||||
+1
-1
@@ -171,7 +171,7 @@ async def startup() -> None:
|
||||
# There is some issue with the uvicorn worker that causes the process to hang when running alembic directly.
|
||||
# See: https://github.com/sqlalchemy/alembic/discussions/1155
|
||||
project_root = Path(__file__).parent.parent
|
||||
subprocess.run(["alembic", "upgrade", "head"], check=True, cwd=project_root) # noqa: S603, S607, ASYNC221
|
||||
subprocess.run(["alembic", "upgrade", "head"], check=True, cwd=project_root) # noqa: ASYNC221, S607
|
||||
|
||||
# Setup scheduler
|
||||
schedule = Scheduler()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Prometheus metrics collectors."""
|
||||
|
||||
import logging
|
||||
from typing import Callable
|
||||
from collections.abc import Callable
|
||||
|
||||
import sqlalchemy
|
||||
from prometheus_client import REGISTRY, Gauge, make_asgi_app
|
||||
|
||||
@@ -291,7 +291,7 @@ def assert_lists_compatible(a: Iterable[dict[str, Any]], b: Iterable[dict[str, A
|
||||
if len(a_sorted) != len(b_sorted):
|
||||
pytest.fail(f"Lists have different lengths: {len(a_sorted)} != {len(b_sorted)}")
|
||||
|
||||
for a_filament, b_filament in zip(a_sorted, b_sorted):
|
||||
for a_filament, b_filament in zip(a_sorted, b_sorted, strict=True):
|
||||
assert_dicts_compatible(a_filament, b_filament)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user