Merge pull request #809 from Donkie/uv

Multiple maintenance updates
This commit is contained in:
Donkie authored and GitHub committed 2026-01-18 22:29:54 +01:00
commit 7cc3bef46f
98 files changed
+2485 -2210

No files matched your search

+12 -6
View File
@@ -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
View File
@@ -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 ⚠️"
+12 -11
View File
@@ -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: |
+1
View File
@@ -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
-24
View File
@@ -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
View File
@@ -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."
+1 -1
View File
@@ -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:**
![image](https://github.com/Donkie/Spoolman/assets/2332094/33928d5e-440f-4445-aca9-456c4370ad0d)
-12
View File
@@ -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",
},
};
+6
View File
@@ -0,0 +1,6 @@
public/locales
dist
dev-dist
node_modules
.vscode
.vite
+23
View File
@@ -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
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
+542 -232
View File
File diff suppressed because it is too large. Load diff
+15 -10
View File
@@ -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": [
+1
View File
@@ -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
View File
@@ -41,7 +41,7 @@ const LoadableResourcePage = loadable(
{
fallback: <div>Page is Loading...</div>,
cacheKey: (props: ResourcePageProps) => `${props.resource}-${props.page}-${props.mode ?? ""}`,
}
},
);
interface LoadablePageProps {
+5 -5
View File
@@ -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,
+1 -1
View File
@@ -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}`;
+3 -3
View File
@@ -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,
+1 -1
View File
@@ -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();
+2 -6
View File
@@ -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={() => (
+1 -1
View File
@@ -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)) {
+2 -2
View File
@@ -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 -2
View File
@@ -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 (
+1 -1
View File
@@ -43,7 +43,7 @@ export function useSpoolmanFilamentFilter(enabled: boolean = false) {
height: "1.4em",
backgroundColor: "#" + filament.color_hex,
}}
></div>
></div>,
);
}
if (filament.material) {
+2 -2
View File
@@ -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();
+4 -4
View File
@@ -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 {
+1 -1
View File
@@ -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 () => {
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -13,5 +13,5 @@ root.render(
<React.Suspense fallback="loading">
<App />
</React.Suspense>
</React.StrictMode>
</React.StrictMode>,
);
+1 -1
View File
@@ -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();
+3 -3
View File
@@ -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);
+2 -2
View File
@@ -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 } = {
+4 -4
View File
@@ -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);
+2 -3
View File
@@ -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);
+2 -3
View File
@@ -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();
+5 -5
View File
@@ -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() {
+4 -4
View File
@@ -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,
});
},
});
+2 -3
View File
@@ -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>
+2 -3
View File
@@ -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();
+7 -6
View File
@@ -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 {
+3 -3
View File
@@ -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;
}
+2 -3
View File
@@ -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();
+1 -1
View File
@@ -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();
+3 -3
View File
@@ -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>({
+3 -3
View File
@@ -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,
+5 -5
View File
@@ -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;
+2 -3
View File
@@ -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();
+1 -1
View File
@@ -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);
+10 -10
View File
@@ -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>
+3 -3
View File
@@ -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) {
+2 -3
View File
@@ -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);
+1 -1
View File
@@ -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) => {
+1 -1
View File
@@ -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;
+3 -3
View File
@@ -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],
});
},
});
+4 -4
View File
@@ -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],
});
},
});
+6 -6
View File
@@ -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" }];
+1 -1
View File
@@ -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) {
+1 -1
View File
@@ -3,7 +3,7 @@ function _isLocalStorageAvailable(): boolean {
localStorage.setItem("test", "test");
localStorage.removeItem("test");
return true;
} catch (e) {
} catch {
return false;
}
}
+49
View File
@@ -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 .
Generated
-1303
View File
File diff suppressed because it is too large. Load diff
+37 -32
View File
@@ -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
View File
@@ -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
-11
View File
@@ -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
#
+1 -2
View File
@@ -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)
-1
View File
@@ -12,7 +12,6 @@ router = APIRouter(
tags=["external"],
)
# ruff: noqa: D103,B008
logger = logging.getLogger(__name__)
+5 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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):
+1 -1
View File
@@ -17,7 +17,7 @@ router = APIRouter(
tags=["other"],
)
# ruff: noqa: D103, B008
# ruff: noqa: D103
@router.get(
+1 -1
View File
@@ -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]},
+5 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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("/")
+8 -9
View File
@@ -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:
+25 -26
View File
@@ -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
View File
@@ -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 -2
View File
@@ -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
View File
@@ -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.
+8 -8
View File
@@ -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:
+9 -10
View File
@@ -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
View File
@@ -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
View File
@@ -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):
+4 -5
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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
+1 -1
View File
@@ -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)
Generated
+1304
View File
File diff suppressed because it is too large. Load diff