1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00

tooling overhaul

This commit is contained in:
thatmattlove
2024-02-27 17:44:19 -05:00
parent ae2753b695
commit cd6bf7a162
60 changed files with 3310 additions and 3539 deletions

1
.gitignore vendored
View File

@@ -16,6 +16,7 @@ __pycache__/
# Pyenv
.python-version
.venv
# MyPy
.mypy_cache

View File

@@ -3,5 +3,5 @@
"eslint.workingDirectories": ["./hyperglass/ui"],
"python.linting.mypyEnabled": false,
"python.linting.enabled": false,
"prettier.configPath": "./hyperglass/ui/.prettierrc"
"biome.lspBin": "./hyperglass/ui/node_modules/.bin/biome"
}

View File

@@ -16,7 +16,7 @@ def build_ui(timeout: int) -> None:
# Project
from hyperglass.state import use_state
from hyperglass.configuration import init_user_config
from hyperglass.util.frontend import build_frontend
from hyperglass.frontend import build_frontend
# Populate configuration to Redis prior to accessing it.
init_user_config()

View File

@@ -11,9 +11,9 @@ from pathlib import Path
# Project
from hyperglass.log import log
from hyperglass.util import copyfiles, check_path, dotenv_to_dict
from hyperglass.state import use_state
# Local
from .files import copyfiles, check_path, dotenv_to_dict
if t.TYPE_CHECKING:
# Project
@@ -56,7 +56,6 @@ async def read_package_json() -> t.Dict[str, t.Any]:
package_json_file = Path(__file__).parent.parent / "ui" / "package.json"
try:
with package_json_file.open("r") as file:
package_json = json.load(file)
@@ -82,7 +81,7 @@ async def node_initial(timeout: int = 180, dev_mode: bool = False) -> str:
try:
proc = await asyncio.create_subprocess_shell(
cmd="yarn --silent --emoji false",
cmd="pnpm install",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=ui_path,
@@ -178,10 +177,8 @@ def generate_opengraph(
log.debug("Copied {} to {}", str(image_path), str(target_path))
with Image.open(copied) as src:
# Only resize the image if it needs to be resized
if src.size[0] != max_width or src.size[1] != max_height:
# Resize image while maintaining aspect ratio
log.debug("Opengraph image is not 1200x630, resizing...")
src.thumbnail((max_width, max_height))
@@ -290,6 +287,10 @@ async def build_frontend( # noqa: C901
dot_env_file = Path(__file__).parent.parent / "ui" / ".env"
env_config = {}
ui_config_file = Path(__file__).parent.parent / "ui" / "hyperglass.json"
ui_config_file.write_text(params.export_json(by_alias=True))
package_json = await read_package_json()
# Set NextJS production/development mode and base URL based on

View File

@@ -52,6 +52,12 @@ HyperglassConsole = Console(
"warning": "bold yellow",
"error": "bold red",
"success": "bold green",
"critical": "bold bright_red",
"logging.level.info": "bold cyan",
"logging.level.warning": "bold yellow",
"logging.level.error": "bold red",
"logging.level.critical": "bold bright_red",
"logging.level.success": "bold green",
"subtle": "rgb(128,128,128)",
}
)
@@ -146,12 +152,13 @@ def init_logger(level: str = "INFO"):
if sys.stdout.isatty():
# Use Rich for logging if hyperglass started from a TTY.
_loguru_logger.add(
sink=RichHandler(
console=HyperglassConsole,
rich_tracebacks=True,
level=level,
tracebacks_show_locals=True,
tracebacks_show_locals=level == "DEBUG",
log_time_format="[%Y%m%d %H:%M:%S]",
),
format=_FMT_BASIC,

View File

@@ -12,9 +12,8 @@ from gunicorn.arbiter import Arbiter # type: ignore
from gunicorn.app.base import BaseApplication # type: ignore
# Local
from .log import CustomGunicornLogger, log, init_logger, setup_lib_logging
from .log import log, init_logger, setup_lib_logging
from .util import get_node_version
from .plugins import InputPluginManager, OutputPluginManager, register_plugin, init_builtin_plugins
from .constants import MIN_NODE_VERSION, MIN_PYTHON_VERSION, __version__
# Ensure the Python version meets the minimum requirements.
@@ -34,12 +33,18 @@ if node_major < MIN_NODE_VERSION:
from .util import cpu_count
from .state import use_state
from .settings import Settings
from .configuration import init_user_config
from .util.frontend import build_frontend
log_level = "INFO" if Settings.debug is False else "DEBUG"
setup_lib_logging(log_level)
init_logger(log_level)
async def build_ui() -> bool:
"""Perform a UI build prior to starting the application."""
from .frontend import build_frontend
state = use_state()
await build_frontend(
dev_mode=Settings.dev_mode,
@@ -54,6 +59,8 @@ async def build_ui() -> bool:
def register_all_plugins() -> None:
"""Validate and register configured plugins."""
from .plugins import register_plugin, init_builtin_plugins
state = use_state()
# Register built-in plugins.
@@ -78,6 +85,8 @@ def register_all_plugins() -> None:
def unregister_all_plugins() -> None:
"""Unregister all plugins."""
from .plugins import InputPluginManager, OutputPluginManager
for manager in (InputPluginManager, OutputPluginManager):
manager().reset()
@@ -91,7 +100,12 @@ def on_starting(server: "Arbiter") -> None:
register_all_plugins()
asyncio.run(build_ui())
if not Settings.disable_ui:
asyncio.run(build_ui())
def when_ready(server: "Arbiter") -> None:
"""Gunicorn post-start hook."""
log.success(
"Started hyperglass {} on http://{} with {!s} workers",
@@ -141,6 +155,8 @@ class HyperglassWSGI(BaseApplication):
def start(*, log_level: str, workers: int, **kwargs) -> None:
"""Start hyperglass via gunicorn."""
from .log import CustomGunicornLogger
HyperglassWSGI(
app="hyperglass.api:app",
options={
@@ -152,6 +168,7 @@ def start(*, log_level: str, workers: int, **kwargs) -> None:
"loglevel": log_level,
"bind": Settings.bind(),
"on_starting": on_starting,
"when_ready": when_ready,
"command": shutil.which("gunicorn"),
"logger_class": CustomGunicornLogger,
"worker_class": "uvicorn.workers.UvicornWorker",
@@ -163,21 +180,15 @@ def start(*, log_level: str, workers: int, **kwargs) -> None:
def run(_workers: int = None):
"""Run hyperglass."""
try:
init_user_config()
from .configuration import init_user_config
try:
log.debug("System settings: {!r}", Settings)
workers, log_level = 1, "DEBUG"
init_user_config()
if Settings.debug is False:
workers, log_level = cpu_count(2), "WARNING"
workers = 1 if Settings.debug else cpu_count(2)
if _workers is not None:
workers = _workers
init_logger(log_level)
setup_lib_logging(log_level)
start(log_level=log_level, workers=workers)
except Exception as error:
# Handle app exceptions.

View File

@@ -121,7 +121,6 @@ class Params(ParamsPublic, HyperglassModel):
return self.export_dict(
include={
"cache": {"show_text", "timeout"},
"debug": ...,
"developer_mode": ...,
"primary_asn": ...,
"request_timeout": ...,

View File

@@ -142,7 +142,6 @@ class ConfigPathItem(Path):
value = Settings.default_app_path.joinpath(
*(p for p in value.parts if p not in Settings.app_path.parts)
)
print(f"{value=}")
return value
def __repr__(self):

View File

@@ -43,6 +43,7 @@ class HyperglassSettings(BaseSettings):
debug: bool = False
dev_mode: bool = False
disable_ui: bool = False
app_path: DirectoryPath = _default_app_path
redis_host: str = "localhost"
redis_password: t.Optional[SecretStr]
@@ -58,7 +59,6 @@ class HyperglassSettings(BaseSettings):
super().__init__(**kwargs)
if self.container:
self.app_path = self.default_app_path
print(self)
def __rich_console__(self, console: "Console", options: "ConsoleOptions") -> "RenderResult":
"""Render a Rich table representation of hyperglass settings."""

View File

@@ -1,5 +1,6 @@
.DS_Store
.env*
hyperglass.json
custom.*[js, html]
*.tsbuildinfo
# dev/test files

40
hyperglass/ui/biome.json Normal file
View File

@@ -0,0 +1,40 @@
{
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
"organizeImports": {
"enabled": true
},
"files": {
"ignore": ["node_modules", "dist", ".next/", "favicon-formats.ts", "custom.*[js, html]"]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": {
"noUselessTypeConstraint": "off",
"noBannedTypes": "off"
},
"style": {
"noInferrableTypes": "off",
"noNonNullAssertion": "off"
},
"correctness": {
"useExhaustiveDependencies": "off"
}
}
},
"formatter": {
"indentStyle": "space",
"lineWidth": 100,
"indentWidth": 2
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"bracketSpacing": true,
"semicolons": "always",
"arrowParentheses": "asNeeded",
"trailingComma": "all"
}
}
}

View File

@@ -17,7 +17,7 @@ import {
useColorMode,
useColorValue,
useBreakpointValue,
useHyperglassConfig,
// useHyperglassConfig,
} from '~/hooks';
import type { UseDisclosureReturn } from '@chakra-ui/react';
@@ -56,7 +56,7 @@ export const Debugger = (): JSX.Element => {
useBreakpointValue({ base: 'SMALL', md: 'MEDIUM', lg: 'LARGE', xl: 'X-LARGE' }) ?? 'UNKNOWN';
const tagSize = useBreakpointValue({ base: 'sm', lg: 'lg' }) ?? 'lg';
const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' }) ?? 'sm';
const { refetch } = useHyperglassConfig();
// const { refetch } = useHyperglassConfig();
return (
<>
<HStack
@@ -92,14 +92,14 @@ export const Debugger = (): JSX.Element => {
>
View Theme
</Button>
<Button
{/* <Button
size={btnSize}
colorScheme="purple"
leftIcon={<DynamicIcon icon={{ hi: 'HiOutlineDownload' }} />}
onClick={() => refetch()}
>
Reload Config
</Button>
</Button> */}
<Tag size={tagSize} colorScheme="teal">
{mediaSize}
</Tag>

View File

@@ -61,7 +61,8 @@ export const Footer = (): JSX.Element => {
icon.rightIcon = <DynamicIcon icon={{ go: 'GoLinkExternal' }} />;
}
return <FooterLink key={item.title} href={url} title={item.title} {...icon} />;
} else if (isMenu(item)) {
}
if (isMenu(item)) {
return (
<FooterButton key={item.title} side="left" content={item.content} title={item.title} />
);
@@ -77,7 +78,8 @@ export const Footer = (): JSX.Element => {
icon.rightIcon = <DynamicIcon icon={{ go: 'GoLinkExternal' }} />;
}
return <FooterLink key={item.title} href={url} title={item.title} {...icon} />;
} else if (isMenu(item)) {
}
if (isMenu(item)) {
return (
<FooterButton key={item.title} side="right" content={item.content} title={item.title} />
);

View File

@@ -55,10 +55,10 @@ export const LocationCard = (props: LocationCardProps): JSX.Element => {
? // Highlight red when there are no overlapping query types for the locations selected.
errorBorder
: isChecked && !hasError
? // Highlight blue when any location is selected and there is no error.
checkedBorder
: // Otherwise, no border.
'transparent',
? // Highlight blue when any location is selected and there is no error.
checkedBorder
: // Otherwise, no border.
'transparent',
[hasError, isChecked, checkedBorder, errorBorder],
);

View File

@@ -103,23 +103,24 @@ export const LookingGlassForm = (): JSX.Element => {
const isFqdn = isFqdnQuery(form.queryTarget, directive?.fieldType ?? null);
if (greetingReady && !isFqdn) {
return setStatus('results');
setStatus('results');
return;
}
if (greetingReady && isFqdn) {
setLoading(true);
return resolvedOpen();
} else {
console.group('%cSomething went wrong', 'color:red;');
console.table({
'Greeting Required': web.greeting.required,
'Greeting Ready': greetingReady,
'Query Target': form.queryTarget,
'Query Type': form.queryType,
'Is FQDN': isFqdn,
});
console.groupEnd();
resolvedOpen();
return;
}
console.group('%cSomething went wrong', 'color:red;');
console.table({
'Greeting Required': web.greeting.required,
'Greeting Ready': greetingReady,
'Query Target': form.queryTarget,
'Query Type': form.queryType,
'Is FQDN': isFqdn,
});
console.groupEnd();
}
const handleLocChange = (locations: string[]) =>

View File

@@ -25,5 +25,5 @@ export const Cell = (props: CellProps): JSX.Element => {
rpki_state: <RPKIState state={data.value} active={data.row.values.active} />,
weight: <Weight weight={data.value} winningWeight={rawData.winning_weight} />,
};
return component[cellId] ?? <> </>;
return component[cellId] ?? '';
};

View File

@@ -119,6 +119,7 @@ export const ASPath = (props: ASPathProps): JSX.Element => {
paths.push(
<DynamicIcon
icon={{ fa: 'FaChevronRight' }}
// biome-ignore lint/suspicious/noArrayIndexKey: index makes sense in this case.
key={`separator-${i}`}
color={color[+active]}
boxSize={5}
@@ -127,6 +128,7 @@ export const ASPath = (props: ASPathProps): JSX.Element => {
/>,
);
paths.push(
// biome-ignore lint/suspicious/noArrayIndexKey: index makes sense in this case.
<Text fontSize="sm" as="span" whiteSpace="pre" fontFamily="mono" key={`as-${asnStr}-${i}`}>
{asnStr}
</Text>,

View File

@@ -32,12 +32,14 @@ function buildOptions(devices: DeviceGroup[]): OptionGroup<LocationOption>[] {
avatar: loc.avatar,
description: loc.description,
},
} as SingleOption),
}) as SingleOption,
)
.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0));
return { label, options };
return { label: label ?? '', options };
})
.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0));
.sort((a, b) =>
(a.label ?? 0) < (b.label ?? 0) ? -1 : (a.label ?? 0) > (b.label ?? 0) ? 1 : 0,
);
}
export const QueryLocation = (props: QueryLocationProps): JSX.Element => {
@@ -58,7 +60,8 @@ export const QueryLocation = (props: QueryLocationProps): JSX.Element => {
const element = useMemo(() => {
if (locationDisplayMode === 'dropdown') {
return 'select';
} else if (locationDisplayMode === 'gallery') {
}
if (locationDisplayMode === 'gallery') {
return 'cards';
}
const groups = options.length;
@@ -159,7 +162,8 @@ export const QueryLocation = (props: QueryLocationProps): JSX.Element => {
)}
</>
);
} else if (element === 'select') {
}
if (element === 'select') {
return (
<Select<LocationOption, true>
isMulti

View File

@@ -60,10 +60,7 @@ function useOptions() {
const filtered = useFormState(s => s.filtered);
return useMemo((): OptionsOrGroup<QueryTypeOption> => {
const groupNames = new Set(
filtered.types
.filter(t => t.groups.length > 0)
.map(t => t.groups)
.flat(),
filtered.types.filter(t => t.groups.length > 0).flatMap(t => t.groups),
);
const optGroups: OptionGroup<QueryTypeOption>[] = Array.from(groupNames).map(group => ({
label: group,

View File

@@ -1,14 +1,14 @@
/* eslint @typescript-eslint/no-explicit-any: 0 */
/* eslint @typescript-eslint/explicit-module-boundary-types: 0 */
// biome-ignore lint/suspicious/noExplicitAny: type guard
export function isStackError(error: any): error is Error {
return typeof error !== 'undefined' && error !== null && 'message' in error;
}
// biome-ignore lint/suspicious/noExplicitAny: type guard
export function isFetchError(error: any): error is Response {
return typeof error !== 'undefined' && error !== null && 'statusText' in error;
}
// biome-ignore lint/suspicious/noExplicitAny: type guard
export function isLGError(error: any): error is QueryResponse {
return typeof error !== 'undefined' && error !== null && 'output' in error;
}
@@ -16,6 +16,7 @@ export function isLGError(error: any): error is QueryResponse {
/**
* Returns true if the response is an LG error, false if not.
*/
// biome-ignore lint/suspicious/noExplicitAny: type guard
export function isLGOutputOrError(data: any): data is QueryResponse {
return typeof data !== 'undefined' && data !== null && data?.level !== 'success';
}

View File

@@ -107,17 +107,20 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, ResultProps> = (
const errorMsg = useMemo(() => {
if (isLGError(error)) {
return error.output as string;
} else if (isLGOutputOrError(data)) {
return data.output as string;
} else if (isFetchError(error)) {
return startCase(error.statusText);
} else if (isStackError(error) && error.message.toLowerCase().startsWith('timeout')) {
return messages.requestTimeout;
} else if (isStackError(error)) {
return startCase(error.message);
} else {
return messages.general;
}
if (isLGOutputOrError(data)) {
return data.output as string;
}
if (isFetchError(error)) {
return startCase(error.statusText);
}
if (isStackError(error) && error.message.toLowerCase().startsWith('timeout')) {
return messages.requestTimeout;
}
if (isStackError(error)) {
return startCase(error.message);
}
return messages.general;
}, [error, data, messages.general, messages.requestTimeout]);
const errorLevel = useMemo<ErrorLevels>(() => {

View File

@@ -45,8 +45,8 @@ export const useControlStyle = <Opt extends SingleOption, IsMulti extends boolea
boxShadow: isError
? `0 0 0 1px ${invalidBorder}`
: isFocused
? `0 0 0 1px ${focusBorder}`
: undefined,
? `0 0 0 1px ${focusBorder}`
: undefined,
'&:hover': { borderColor: isFocused ? focusBorder : borderHover },
'&:hover > div > span': { backgroundColor: borderHover },
'&:focus': { borderColor: isError ? invalidBorder : focusBorder },

View File

@@ -30,7 +30,7 @@ export const TableRow = (props: TableRowProps): JSX.Element => {
{ borderTop: '1px', borderTopColor: 'blackAlpha.100' },
{ borderTop: '1px', borderTopColor: 'whiteAlpha.100' },
);
let bg;
let bg = undefined;
if (highlight) {
bg = `${String(highlightBg)}.${alpha}`;

View File

@@ -6,6 +6,7 @@
export const CustomJavascript = (props: React.PropsWithChildren<Dict>): JSX.Element => {
const { children } = props;
if (typeof children === 'string' && children !== '') {
// biome-ignore lint/security/noDangerouslySetInnerHtml: required for injecting custom JS
return <script id="custom-javascript" dangerouslySetInnerHTML={{ __html: children }} />;
}
return <></>;
@@ -19,6 +20,7 @@ export const CustomJavascript = (props: React.PropsWithChildren<Dict>): JSX.Elem
export const CustomHtml = (props: React.PropsWithChildren<Dict>): JSX.Element => {
const { children } = props;
if (typeof children === 'string' && children !== '') {
// biome-ignore lint/security/noDangerouslySetInnerHtml: required for injecting custom HTML
return <div id="custom-html" dangerouslySetInnerHTML={{ __html: children }} />;
}
return <></>;

View File

@@ -47,7 +47,7 @@ class IconError extends Error {
this.original = original;
this.library = library;
this.iconName = iconName;
this.stack = this.stack + `\nOriginal object: '${JSON.stringify(this.original)}'`;
this.stack += `\nOriginal object: '${JSON.stringify(this.original)}'`;
}
get message(): string {

View File

@@ -68,7 +68,7 @@ type MDProps = {
node: Dict;
};
/* eslint @typescript-eslint/no-explicit-any: off */
// biome-ignore lint/suspicious/noExplicitAny: reasons!
function hasNode<C>(p: any): p is C & MDProps {
return 'node' in p;
}

View File

@@ -1,3 +1,4 @@
import { expect, describe, it } from 'vitest';
import { renderHook } from '@testing-library/react';
import '@testing-library/jest-dom';
import { useBooleanValue } from './use-boolean-value';

View File

@@ -11,8 +11,7 @@ export function useBooleanValue<T extends unknown, F extends unknown>(
return useMemo(() => {
if (status) {
return ifTrue;
} else {
return ifFalse;
}
return ifFalse;
}, [status, ifTrue, ifFalse]);
}

View File

@@ -1,3 +1,4 @@
import { expect, describe, it } from 'vitest';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import { useDevice } from './use-device';

View File

@@ -14,10 +14,7 @@ export type UseDeviceReturn = (
export function useDevice(): UseDeviceReturn {
const { devices } = useConfig();
const locations = useMemo<Device[]>(
() => devices.map(group => group.locations).flat(),
[devices],
);
const locations = useMemo<Device[]>(() => devices.flatMap(group => group.locations), [devices]);
function getDevice(id: string): Nullable<Device> {
return locations.find(device => device.id === id) ?? null;

View File

@@ -1,4 +1,5 @@
import 'isomorphic-fetch';
import { expect, describe, it } from 'vitest';
import '@testing-library/jest-dom';
import { renderHook } from '@testing-library/react-hooks';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
@@ -16,7 +17,7 @@ const CloudflareWrapper = (props: React.PropsWithChildren<Dict<JSX.Element>>) =>
cache: { timeout: 120 },
web: { dnsProvider: { url: 'https://cloudflare-dns.com/dns-query' } },
} as jest.Mocked<Config>;
} as Config;
return (
<QueryClientProvider client={queryClient}>
<HyperglassContext.Provider value={config} {...props} />
@@ -28,7 +29,7 @@ const GoogleWrapper = (props: React.PropsWithChildren<Dict<JSX.Element>>) => {
const config = {
cache: { timeout: 120 },
web: { dnsProvider: { url: 'https://dns.google/resolve' } },
} as jest.Mocked<Config>;
} as Config;
return (
<QueryClientProvider client={queryClient}>
<HyperglassContext.Provider value={config} {...props} />

View File

@@ -21,7 +21,7 @@ const query: QueryFunction<DnsOverHttps.Response, DNSQueryKey> = async (
const controller = new AbortController();
let json;
let json = undefined;
const type = family === 4 ? 'A' : family === 6 ? 'AAAA' : '';
if (url !== null) {

View File

@@ -61,10 +61,7 @@ interface FormStateType<Opt extends SingleOption = SingleOption> {
setSelection<
Opt extends SingleOption,
K extends keyof FormSelections<Opt> = keyof FormSelections<Opt>,
>(
field: K,
value: FormSelections[K],
): void;
>(field: K, value: FormSelections[K]): void;
setTarget(update: Partial<Target>): void;
getDirective(): Directive | null;
reset(): void;
@@ -155,7 +152,7 @@ const formState: StateCreator<FormStateType> = (set, get) => ({
// Determine all unique group names.
const allGroups = allDevices.map(dev =>
Array.from(new Set(dev.directives.map(dir => dir.groups).flat())),
Array.from(new Set(dev.directives.flatMap(dir => dir.groups))),
);
// Get group names that are common between all selected locations.

View File

@@ -1,6 +1,7 @@
import { expect, describe, it } from 'vitest';
import '@testing-library/jest-dom';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { userEvent } from '@testing-library/user-event';
import { useGreeting } from './use-greeting';
const TRUE = JSON.stringify(true);
@@ -26,16 +27,16 @@ const TestComponent = (): JSX.Element => {
Close
</button>
<button id="ack-false-required" type="button" onClick={() => ack(false, true)}>
{`Don't acknowledge, is required`}
Don't acknowledge, is required
</button>
<button id="ack-true-required" type="button" onClick={() => ack(true, true)}>
{`Acknowledge, is required`}
Acknowledge, is required
</button>
<button id="ack-false-not-required" type="button" onClick={() => ack(false, false)}>
{`Don't Acknowledge, not required`}
Don't Acknowledge, not required
</button>
<button id="ack-true-not-required" type="button" onClick={() => ack(true, false)}>
{`Acknowledge, not required`}
Acknowledge, not required
</button>
</div>
);

View File

@@ -1,6 +1,7 @@
import { expect, describe, it } from 'vitest';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';
import { userEvent } from '@testing-library/user-event';
import { ChakraProvider, useColorMode, useColorModeValue, extendTheme } from '@chakra-ui/react';
import { useOpposingColor } from './use-opposing-color';
@@ -32,17 +33,17 @@ describe('useOpposingColor Hook', () => {
const test1 = container.querySelector('#test1');
const test2 = container.querySelector('#test2');
expect(test1).toHaveStyle('color: black;');
expect(test2).toHaveStyle('color: black;');
expect(test1).toHaveStyle('color: rgb(0, 0, 0);');
expect(test2).toHaveStyle('color: rgb(0, 0, 0);');
await userEvent.click(getByRole('button'));
expect(test1).toHaveStyle('color: white;');
expect(test2).toHaveStyle('color: white;');
expect(test1).toHaveStyle('color: rgb(255, 255, 255);');
expect(test2).toHaveStyle('color: rgb(255, 255, 255);');
await userEvent.click(getByRole('button'));
expect(test1).toHaveStyle('color: black;');
expect(test2).toHaveStyle('color: black;');
expect(test1).toHaveStyle('color: rgb(0, 0, 0);');
expect(test2).toHaveStyle('color: rgb(0, 0, 0);');
});
});

View File

@@ -22,12 +22,13 @@ export function useIsDarkCallback(): UseIsDarkCallbackReturn {
const theme = useTheme();
return useCallback(
(color: string): boolean => {
let opposing = color;
if (typeof color === 'string' && color.match(/[a-zA-Z]+\.[a-zA-Z0-9]+/g)) {
color = getColor(theme, color, color);
opposing = getColor(theme, color, color);
}
let opposingShouldBeDark = true;
try {
opposingShouldBeDark = isLight(color)(theme);
opposingShouldBeDark = isLight(opposing)(theme);
} catch (err) {
console.error(err);
}
@@ -46,9 +47,8 @@ export function useOpposingColor(color: string, options?: OpposingColorOptions):
return useMemo(() => {
if (isBlack) {
return options?.dark ?? 'black';
} else {
return options?.light ?? 'white';
}
return options?.light ?? 'white';
}, [isBlack, options?.dark, options?.light]);
}

View File

@@ -1,3 +1,4 @@
import { expect, describe, it } from 'vitest';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import { useStrf } from './use-strf';

View File

@@ -29,7 +29,7 @@ function formatAsPath(path: number[]): string {
function formatCommunities(comms: string[]): string {
const commsStr = comms.map(c => ` - ${c}`);
return '\n' + commsStr.join('\n');
return `\n ${commsStr.join('\n')}`;
}
function formatBool(val: boolean): string {
@@ -85,9 +85,8 @@ export function useTableToString(
function getFmtFunc(accessor: keyof Route): TableToStringFormatter {
if (isFormatted(accessor)) {
return tableFormatMap[accessor];
} else {
return String;
}
return String;
}
function doFormat(target: string[], data: QueryResponse | undefined): string {

View File

@@ -1,31 +0,0 @@
/**
* Jest Testing Configuration
*
* @see https://nextjs.org/docs/testing
* @type {import('@jest/types').Config.InitialOptions}
*/
const jestConfig = {
collectCoverageFrom: ['**/*.{ts,tsx}', '!**/*.d.ts', '!**/node_modules/**'],
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/.next/'],
testEnvironment: 'jsdom',
transform: { '^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }] },
transformIgnorePatterns: ['/node_modules/', '^.+\\.module\\.(css|sass|scss)$'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
'^@/components/(.*)$': '<rootDir>/components/$1',
'^~/components': ['components/index'],
'^~/components/(.*)$': '<rootDir>/components/$1',
'^~/context': '<rootDir>/context/index',
'^~/context/(.*)$': '<rootDir>/context/$1',
'^~/hooks': '<rootDir>/hooks/index',
'^~/hooks/(.*)$': '<rootDir>/hooks/$1',
'^~/state': '<rootDir>/state/index',
'^~/state/(.*)$': '<rootDir>/state/$1',
'^~/types': '<rootDir>/types/index',
'^~/types/(.*)$': '<rootDir>/types/$1',
'^~/util': '<rootDir>/util/index',
'^~/util/(.*)$': '<rootDir>/util/$1',
},
};
module.exports = jestConfig;

View File

@@ -1 +0,0 @@
import '@testing-library/jest-dom/extend-expect';

View File

@@ -20,18 +20,19 @@ app
const devProxy = {
'/api/query/': {
target: process.env.HYPERGLASS_URL + 'api/query/',
target: `${process.env.HYPERGLASS_URL}api/query/`,
pathRewrite: { '^/api/query/': '' },
},
'/ui/props/': {
target: process.env.HYPERGLASS_URL + 'ui/props/',
target: `${process.env.HYPERGLASS_URL}ui/props/`,
pathRewrite: { '^/ui/props/': '' },
},
'/images': { target: process.env.HYPERGLASS_URL + 'images', pathRewrite: { '^/images': '' } },
'/images': { target: `${process.env.HYPERGLASS_URL}images`, pathRewrite: { '^/images': '' } },
};
// Set up the proxy.
if (dev) {
// biome-ignore lint/complexity/noForEach: not messing with Next's example code.
Object.keys(devProxy).forEach(context => {
server.use(proxyMiddleware(context, devProxy[context]));
});

View File

@@ -1,91 +1,94 @@
{
"version": "2.0.0-dev",
"name": "ui",
"description": "UI for hyperglass, the modern network looking glass",
"author": "Matt Love",
"license": "BSD-3-Clause-Clear",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts --ext .tsx",
"dev": "node nextdev",
"start": "next start",
"typecheck": "tsc --noEmit",
"format": "prettier --config ./.prettierrc -c -w .",
"format:check": "prettier --config ./.prettierrc -c .",
"build": "export NODE_OPTIONS=--openssl-legacy-provider; next build && next export -o ../hyperglass/static/ui",
"test": "jest"
},
"browserslist": "> 0.25%, not dead",
"dependencies": {
"@chakra-ui/react": "^2.5.5",
"@chakra-ui/theme": "3.0.1",
"@chakra-ui/theme-tools": "^2.0.17",
"@chakra-ui/utils": "^2.0.15",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@hookform/devtools": "^4.3.0",
"@hookform/resolvers": "^2.9.10",
"@tanstack/react-query": "^4.22.0",
"dagre": "^0.8.5",
"dayjs": "^1.10.4",
"framer-motion": "^10.11.6",
"lodash": "^4.17.21",
"merge-anything": "^4.0.1",
"next": "12.3.4",
"palette-by-numbers": "^0.1.6",
"plur": "^4.0.0",
"react": "^18.2.0",
"react-countdown": "^2.3.0",
"react-device-detect": "^1.15.0",
"react-dom": "^18.2.0",
"react-fast-compare": "^3.2.1",
"react-flow-renderer": "^10.3.17",
"react-hook-form": "^7.42.1",
"react-icons": "^4.3.1",
"react-if": "^4.1.4",
"react-markdown": "^5.0.3",
"react-select": "^5.7.0",
"react-string-replace": "^0.5.0",
"react-table": "^7.7.0",
"remark-gfm": "^1.0.0",
"string-format": "^2.0.0",
"vest": "^3.2.8",
"zustand": "^3.7.2"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "^7.0.2",
"@testing-library/user-event": "^14.4.3",
"@types/dagre": "^0.7.44",
"@types/express": "^4.17.13",
"@types/lodash": "^4.14.177",
"@types/node": "^18.15.11",
"@types/react": "^18.0.35",
"@types/react-table": "^7.7.1",
"@types/string-format": "^2.0.0",
"@typescript-eslint/eslint-plugin": "^4.31.0",
"@typescript-eslint/parser": "^4.31.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^27.2.1",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-typescript": "^2.4.0",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-json": "^3.1.0",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.25.1",
"eslint-plugin-react-hooks": "^4.2.0",
"express": "^4.17.1",
"http-proxy-middleware": "0.20.0",
"identity-obj-proxy": "^3.0.0",
"isomorphic-fetch": "^3.0.0",
"jest": "^27.2.1",
"prettier": "^2.3.2",
"prettier-eslint": "^13.0.0",
"react-test-renderer": "^18.2.0",
"type-fest": "^3.8.0",
"typescript": "^5.0.4"
}
"version": "2.0.0-dev",
"name": "ui",
"description": "UI for hyperglass, the modern network looking glass",
"author": "Matt Love",
"license": "BSD-3-Clause-Clear",
"private": true,
"scripts": {
"lint": "biome lint .",
"dev": "node nextdev",
"start": "next start",
"typecheck": "tsc --noEmit",
"format": "biome format --write .",
"format:check": "biome format .",
"build": "export NODE_OPTIONS=--openssl-legacy-provider; next build && next export -o ../hyperglass/static/ui",
"test": "vitest --run"
},
"browserslist": "> 0.25%, not dead",
"dependencies": {
"@chakra-ui/react": "^2.5.5",
"@chakra-ui/theme": "3.0.1",
"@chakra-ui/theme-tools": "^2.0.17",
"@chakra-ui/utils": "^2.0.15",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@hookform/devtools": "^4.3.0",
"@hookform/resolvers": "^2.9.10",
"@tanstack/react-query": "^4.22.0",
"dagre": "^0.8.5",
"dayjs": "^1.10.4",
"framer-motion": "^10.11.6",
"lodash": "^4.17.21",
"merge-anything": "^4.0.1",
"next": "13.5.6",
"palette-by-numbers": "^0.1.6",
"plur": "^4.0.0",
"react": "^18.2.0",
"react-countdown": "^2.3.0",
"react-device-detect": "^1.15.0",
"react-dom": "^18.2.0",
"react-fast-compare": "^3.2.1",
"react-flow-renderer": "^10.3.17",
"react-hook-form": "^7.42.1",
"react-icons": "^4.3.1",
"react-if": "^4.1.4",
"react-markdown": "^5.0.3",
"react-select": "^5.7.0",
"react-string-replace": "^0.5.0",
"react-table": "^7.7.0",
"remark-gfm": "^1.0.0",
"string-format": "^2.0.0",
"vest": "^3.2.8",
"zustand": "^3.7.2"
},
"devDependencies": {
"@biomejs/biome": "1.5.3",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.2.1",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/dagre": "^0.7.44",
"@types/express": "^4.17.21",
"@types/lodash": "^4.14.177",
"@types/node": "^20.11.20",
"@types/react": "^18.2.60",
"@types/react-table": "^7.7.1",
"@types/string-format": "^2.0.0",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/ui": "^1.3.1",
"babel-eslint": "^10.1.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-json": "^3.1.0",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"express": "^4.18.2",
"http-proxy-middleware": "2.0.6",
"identity-obj-proxy": "^3.0.0",
"isomorphic-fetch": "^3.0.0",
"jsdom": "^24.0.0",
"prettier": "^3.2.5",
"prettier-eslint": "^16.3.0",
"react-test-renderer": "^18.2.0",
"type-fest": "^4.10.3",
"typescript": "^5.3.3",
"vitest": "^1.3.1"
}
}

View File

@@ -1,45 +1,23 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Switch, Case, Default } from 'react-if';
import { Meta, Layout } from '~/components';
import { HyperglassProvider } from '~/context';
import { LoadError, Loading } from '~/elements';
import { useHyperglassConfig } from '~/hooks';
import * as config from '../hyperglass.json';
import type { AppProps } from 'next/app';
import type { Config } from '~/types';
const queryClient = new QueryClient();
const AppComponent = (props: AppProps) => {
const { Component, pageProps } = props;
const { data, error, isLoading, ready, refetch, showError, isLoadingInitial } =
useHyperglassConfig();
return (
<Switch>
<Case condition={isLoadingInitial}>
<Loading />
</Case>
<Case condition={showError}>
<LoadError error={error!} retry={refetch} inProgress={isLoading} />
</Case>
<Case condition={ready}>
<HyperglassProvider config={data!}>
<Meta />
<Layout>
<Component {...pageProps} />
</Layout>
</HyperglassProvider>
</Case>
<Default>
<LoadError error={error!} retry={refetch} inProgress={isLoading} />
</Default>
</Switch>
);
};
const App = (props: AppProps): JSX.Element => {
const { Component, pageProps } = props;
return (
<QueryClientProvider client={queryClient}>
<AppComponent {...props} />
<HyperglassProvider config={config as unknown as Config}>
<Meta />
<Layout>
<Component {...pageProps} />
</Layout>
</HyperglassProvider>
</QueryClientProvider>
);
};

View File

@@ -2,8 +2,9 @@ import fs from 'fs';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ColorModeScript } from '@chakra-ui/react';
import { CustomJavascript, CustomHtml, Favicon } from '~/elements';
import { getHyperglassConfig, googleFontUrl } from '~/util';
import { googleFontUrl } from '~/util';
import favicons from '../favicon-formats';
import config from '../hyperglass.json';
import type { DocumentContext, DocumentInitialProps } from 'next/document';
import type { ThemeConfig } from '~/types';
@@ -18,8 +19,8 @@ interface DocumentExtra
class MyDocument extends Document<DocumentExtra> {
static async getInitialProps(ctx: DocumentContext): Promise<DocumentExtra> {
const initialProps = await Document.getInitialProps(ctx);
let customJs = '',
customHtml = '';
let customJs = '';
let customHtml = '';
if (fs.existsSync('custom.js')) {
customJs = fs.readFileSync('custom.js').toString();
@@ -31,18 +32,18 @@ class MyDocument extends Document<DocumentExtra> {
let fonts = { body: '', mono: '' };
let defaultColorMode: 'light' | 'dark' | null = null;
const hyperglassUrl = process.env.HYPERGLASS_URL ?? '';
const {
web: {
theme: { fonts: themeFonts, defaultColorMode: themeDefaultColorMode },
},
} = await getHyperglassConfig(hyperglassUrl);
// const hyperglassUrl = process.env.HYPERGLASS_URL ?? '';
// const {
// web: {
// theme: { fonts: themeFonts, defaultColorMode: themeDefaultColorMode },
// },
// } = await getHyperglassConfig(hyperglassUrl);
fonts = {
body: googleFontUrl(themeFonts.body),
mono: googleFontUrl(themeFonts.mono),
body: googleFontUrl(config.web.theme.fonts.body),
mono: googleFontUrl(config.web.theme.fonts.mono),
};
defaultColorMode = themeDefaultColorMode;
defaultColorMode = config.web.theme.defaultColorMode;
return { customJs, customHtml, fonts, defaultColorMode, ...initialProps };
}
@@ -63,8 +64,8 @@ class MyDocument extends Document<DocumentExtra> {
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
<link href={this.props.fonts.mono} rel="stylesheet" />
<link href={this.props.fonts.body} rel="stylesheet" />
{favicons.map((favicon, idx) => (
<Favicon key={idx} {...favicon} />
{favicons.map(favicon => (
<Favicon key={JSON.stringify(favicon)} {...favicon} />
))}
<CustomJavascript>{this.props.customJs}</CustomJavascript>
</Head>

File diff suppressed because it is too large Load Diff

View File

@@ -40,6 +40,7 @@
"**/*.tsx",
"types/*.d.ts",
"next.config.js",
"nextdev.js"
"nextdev.js",
"hyperglass.json"
]
}

View File

@@ -1,7 +1,7 @@
import type { Theme } from './theme';
import type { CamelCasedPropertiesDeep, CamelCasedProperties } from 'type-fest';
type Side = 'left' | 'right';
type Side = 'left' | 'right' | string;
export type ParsedDataField = [string, keyof Route, 'left' | 'right' | 'center' | null];
@@ -92,19 +92,17 @@ interface _Web {
links: _Link[];
menus: _Menu[];
greeting: _Greeting;
help_menu: { enable: boolean; title: string };
logo: _Logo;
terms: { enable: boolean; title: string };
text: _Text;
theme: _ThemeConfig;
location_display_mode: 'auto' | 'gallery' | 'dropdown';
location_display_mode: 'auto' | 'gallery' | 'dropdown' | string;
highlight: _Highlight[];
}
type _DirectiveBase = {
id: string;
name: string;
field_type: 'text' | 'select' | null;
field_type: 'text' | 'select' | null | string;
description: string;
groups: string[];
info: string | null;
@@ -125,7 +123,7 @@ type _Directive = _DirectiveBase | _DirectiveSelect;
interface _Device {
id: string;
name: string;
group: string;
group: string | null;
avatar: string | null;
directives: _Directive[];
description: string | null;
@@ -144,7 +142,7 @@ interface _Cache {
type _Config = _ConfigDeep & _ConfigShallow;
interface _DeviceGroup {
group: string;
group: string | null;
locations: _Device[];
}
@@ -157,7 +155,6 @@ interface _ConfigDeep {
}
interface _ConfigShallow {
debug: boolean;
developer_mode: boolean;
primary_asn: string;
request_timeout: number;

View File

@@ -55,10 +55,16 @@ export declare global {
export interface ProcessEnv {
hyperglass: { favicons: import('./config').Favicon[]; version: string };
buildId: string;
UI_PARAMS: import('./config').Config;
}
}
}
declare module 'hyperglass.json' {
type Config = import('./config').Config;
export default Config;
}
declare module 'react' {
// Enable generic typing with forwardRef.
// eslint-disable-next-line @typescript-eslint/ban-types

View File

@@ -1,10 +1,13 @@
import { expect, describe, it, test } from 'vitest';
import { all, chunkArray, entries, dedupObjectArray, andJoin, isFQDN } from './common';
test('all - all items are truthy', () => {
// biome-ignore lint/suspicious/noSelfCompare: because this is a test, duh
expect(all(1 === 1, true, 'one' === 'one')).toBe(true);
});
test('all - one item is not truthy', () => {
// biome-ignore lint/suspicious/noSelfCompare: because this is a test, duh
expect(all(1 === 1, false, 'one' === 'one')).toBe(false);
});

View File

@@ -35,6 +35,7 @@ export function entries<O, K extends keyof O = keyof O>(obj: O): [K, O[K]][] {
*/
export async function fetchWithTimeout(
uri: string,
// biome-ignore lint/style/useDefaultParameterLast: goal is to match the fetch API as closely as possible.
options: RequestInit = {},
timeout: number,
controller: AbortController,
@@ -69,9 +70,8 @@ export function dedupObjectArray<E extends Record<string, unknown>, P extends ke
if (!x) {
return acc.concat([current]);
} else {
return acc;
}
return acc;
}, []);
}

View File

@@ -11,7 +11,7 @@ export class ConfigLoadError extends Error {
constructor(detail?: string) {
super();
this.detail = detail;
this.baseMessage = `Unable to connect to hyperglass at`;
this.baseMessage = 'Unable to connect to hyperglass at';
this.message = `${this.baseMessage} '${this.url}'`;
console.error(this);
}
@@ -30,7 +30,7 @@ export async function getHyperglassConfig(url?: QueryFunctionContext | string):
let fetchUrl = '/ui/props/';
if (typeof url === 'string') {
fetchUrl = url.replace(/(^\/)|(\/$)/g, '') + '/ui/props/';
fetchUrl = `${url.replace(/(^\/)|(\/$)/g, '')}/ui/props`;
}
if (process.env.NODE_ENV === 'production') {

View File

@@ -8,7 +8,6 @@ import type { StateCreator, SetState, GetState, StoreApi } from 'zustand';
* @param store zustand store function.
* @param name Store name.
*/
// eslint-disable-next-line @typescript-eslint/ban-types
export function withDev<T extends object = {}>(
store: StateCreator<T>,
name: string,

View File

@@ -1,4 +1,5 @@
import { googleFontUrl } from './theme';
import { describe, expect, test } from 'vitest';
describe('google font URL generation', () => {
test('no space font', () => {

View File

@@ -0,0 +1,18 @@
/// <reference types="vitest" />
import path from 'node:path';
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
},
resolve: {
alias: {
'~': path.resolve(__dirname, './'),
},
},
});

View File

@@ -16,14 +16,13 @@ from .tools import (
run_coroutine_in_new_thread,
)
from .typing import is_type, is_series
from .frontend import build_ui, build_frontend
from .validation import get_driver, resolve_hostname, validate_platform
from .system_info import cpu_count, check_python, get_system_info, get_node_version
__all__ = (
"at_least",
"build_frontend",
"build_ui",
# "build_frontend",
# "build_ui",
"check_path",
"check_python",
"compare_dicts",

View File

@@ -1,70 +1,64 @@
[build-system]
build-backend = "poetry.core.masonry.api"
requires = ["poetry-core"]
[tool.poetry]
authors = ["Matt Love <matt@hyperglass.dev>"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Information Technology",
"Operating System :: POSIX :: Linux",
"Programming Language :: TypeScript",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Internet",
"Topic :: System :: Networking",
]
description = "hyperglass is the modern network looking glass that tries to make the internet better."
documentation = "https://hyperglass.dev"
homepage = "https://hyperglass.dev"
keywords = ["looking glass", "network automation", "isp", "bgp", "routing"]
license = "BSD-3-Clause-Clear"
[project]
name = "hyperglass"
readme = "README.md"
repository = "https://github.com/thatmattlove/hyperglass"
version = "2.0.0-dev"
description = "hyperglass is the modern network looking glass that tries to make the internet better."
authors = [
{ name = "thatmattlove", email = "matt@hyperglass.dev" }
]
dependencies = [
"Pillow==10.2.0",
"PyJWT==2.6.0",
"PyYAML>=6.0",
"aiofiles>=23.2.1",
"distro==1.8.0",
"fastapi==0.95.1",
"favicons==0.2.2",
"gunicorn==20.1.0",
"httpx==0.24.0",
"loguru==0.7.0",
"netmiko==4.1.2",
"paramiko==3.4.0",
"psutil==5.9.4",
"py-cpuinfo==9.0.0",
"pydantic==1.10.14",
"redis==4.5.4",
"rich>=13.7.0",
"typer>=0.9.0",
"uvicorn==0.21.1",
"uvloop>=0.17.0",
"xmltodict==0.13.0",
]
readme = "README.md"
requires-python = ">= 3.11"
[tool.poetry.scripts]
[project.scripts]
hyperglass = "hyperglass.console:run"
[tool.poetry.dependencies]
Pillow = "^9.5.0"
PyJWT = "^2.6.0"
PyYAML = "^6.0"
aiofiles = "^23.1.0"
distro = "^1.8.0"
fastapi = "^0.95.1"
favicons = "^0.2.0"
gunicorn = "^20.1.0"
httpx = "^0.24.0"
loguru = "^0.7.0"
netmiko = "^4.1.2"
paramiko = "^3.1.0"
psutil = "^5.9.4"
py-cpuinfo = "^9.0.0"
pydantic = "^1.10.7"
python = ">=3.8.1,<4.0"
redis = "^4.5.4"
rich = "^13.3.4"
typer = "^0.7.0"
uvicorn = "^0.21.1"
uvloop = "^0.17.0"
xmltodict = "^0.13.0"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.poetry.group.dev.dependencies]
bandit = "^1.7.4"
black = "^22.12.0"
isort = "^5.10.1"
pep8-naming = "^0.13.2"
pre-commit = "^2.20.0"
pytest = "^7.2.0"
pytest-asyncio = "^0.20.3"
pytest-dependency = "^0.5.1"
ruff = "^0.0.261"
stackprinter = "^0.2.10"
taskipy = "^1.10.3"
[tool.rye]
managed = true
dev-dependencies = [
"bandit>=1.7.7",
"black>=24.2.0",
"isort>=5.13.2",
"pep8-naming>=0.13.3",
"pre-commit>=3.6.1",
"pytest>=8.0.1",
"pytest-asyncio>=0.23.5",
"pytest-dependency>=0.6.0",
"ruff>=0.2.1",
"stackprinter>=0.2.11",
"taskipy>=1.12.2",
]
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["hyperglass"]
[tool.black]
line-length = 100
@@ -85,13 +79,6 @@ multi_line_output = 3
profile = "black"
skip_glob = "hyperglass/api/examples/*.py"
[tool.pyright]
exclude = ["**/node_modules", "**/ui", "**/__pycache__"]
include = ["hyperglass"]
pythonVersion = "3.9"
reportMissingImports = true
reportMissingTypeStubs = true
[tool.taskipy.tasks]
check = {cmd = "task lint && task ui-lint", help = "Run all lint checks"}
docs-platforms = {cmd = "python3 -c 'from hyperglass.util.docs import create_platform_list;print(create_platform_list())'"}
@@ -102,12 +89,12 @@ start = {cmd = "python3 -m hyperglass.main", help = "Start hyperglass"}
start-asgi = {cmd = "uvicorn hyperglass.api:app", help = "Start hyperglass via Uvicorn"}
test = {cmd = "pytest hyperglass --ignore hyperglass/plugins/external", help = "Run hyperglass tests"}
ui-build = {cmd = "python3 -m hyperglass.console build-ui", help = "Run a UI Build"}
ui-dev = {cmd = "yarn --cwd ./hyperglass/ui/ dev", help = "Start the Next.JS dev server"}
ui-format = {cmd = "yarn --cwd ./hyperglass/ui/ format", help = "Run Prettier"}
ui-lint = {cmd = "yarn --cwd ./hyperglass/ui/ lint", help = "Run ESLint"}
ui-typecheck = {cmd = "yarn --cwd ./hyperglass/ui/ typecheck", help = "Run TypeScript Check"}
ui-dev = {cmd = "pnpm run --dir ./hyperglass/ui/ dev", help = "Start the Next.JS dev server"}
ui-format = {cmd = "pnpm run --dir ./hyperglass/ui/ format", help = "Run Prettier"}
ui-lint = {cmd = "pnpm run --dir ./hyperglass/ui/ lint", help = "Run ESLint"}
ui-typecheck = {cmd = "pnpm run --dir ./hyperglass/ui/ typecheck", help = "Run TypeScript Check"}
upgrade = {cmd = "python3 version.py", help = "Upgrade hyperglass version"}
yarn = {cmd = "yarn --cwd ./hyperglass/ui/", help = "Run a yarn command from the UI directory"}
pnpm = {cmd = "pnpm run --dir ./hyperglass/ui/", help = "Run a yarn command from the UI directory"}
[tool.ruff]
exclude = [
@@ -148,7 +135,7 @@ convention = "pep257"
[tool.ruff.mccabe]
max-complexity = 10
[tool.ruff.per-file-ignores]
[tool.ruff.lint.per-file-ignores]
"hyperglass/main.py" = ["E402"]
# Disable classmethod warning for validator decorat
"hyperglass/configuration/models/*.py" = ["N805"]

206
requirements-dev.lock Normal file
View File

@@ -0,0 +1,206 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: false
# with-sources: false
-e file:.
aiofiles==23.2.1
# via hyperglass
anyio==4.3.0
# via httpcore
# via starlette
bandit==1.7.7
bcrypt==4.1.2
# via paramiko
black==24.2.0
certifi==2024.2.2
# via httpcore
# via httpx
cffi==1.16.0
# via cryptography
# via pynacl
cfgv==3.4.0
# via pre-commit
chardet==5.2.0
# via reportlab
click==8.1.7
# via black
# via typer
# via uvicorn
colorama==0.4.6
# via taskipy
cryptography==42.0.3
# via paramiko
cssselect2==0.7.0
# via svglib
distlib==0.3.8
# via virtualenv
distro==1.8.0
# via hyperglass
fastapi==0.95.1
# via hyperglass
favicons==0.2.2
# via hyperglass
filelock==3.13.1
# via virtualenv
flake8==7.0.0
# via pep8-naming
freetype-py==2.4.0
# via rlpycairo
future==0.18.3
# via textfsm
gunicorn==20.1.0
# via hyperglass
h11==0.14.0
# via httpcore
# via uvicorn
httpcore==0.17.3
# via httpx
httpx==0.24.0
# via hyperglass
identify==2.5.35
# via pre-commit
idna==3.6
# via anyio
# via httpx
iniconfig==2.0.0
# via pytest
isort==5.13.2
loguru==0.7.0
# via hyperglass
lxml==5.1.0
# via svglib
markdown-it-py==3.0.0
# via rich
mccabe==0.7.0
# via flake8
mdurl==0.1.2
# via markdown-it-py
mypy-extensions==1.0.0
# via black
netmiko==4.1.2
# via hyperglass
nodeenv==1.8.0
# via pre-commit
ntc-templates==4.3.0
# via netmiko
packaging==23.2
# via black
# via pytest
paramiko==3.4.0
# via hyperglass
# via netmiko
# via scp
pathspec==0.12.1
# via black
pbr==6.0.0
# via stevedore
pep8-naming==0.13.3
pillow==10.2.0
# via favicons
# via hyperglass
# via reportlab
platformdirs==4.2.0
# via black
# via virtualenv
pluggy==1.4.0
# via pytest
pre-commit==3.6.2
psutil==5.9.4
# via hyperglass
# via taskipy
py-cpuinfo==9.0.0
# via hyperglass
pycairo==1.26.0
# via rlpycairo
pycodestyle==2.11.1
# via flake8
pycparser==2.21
# via cffi
pydantic==1.10.14
# via fastapi
# via hyperglass
pyflakes==3.2.0
# via flake8
pygments==2.17.2
# via rich
pyjwt==2.6.0
# via hyperglass
pynacl==1.5.0
# via paramiko
pyserial==3.5
# via netmiko
pytest==8.0.1
# via pytest-asyncio
# via pytest-dependency
pytest-asyncio==0.23.5
pytest-dependency==0.6.0
pyyaml==6.0.1
# via bandit
# via hyperglass
# via netmiko
# via pre-commit
redis==4.5.4
# via hyperglass
reportlab==4.1.0
# via favicons
# via svglib
rich==13.7.0
# via bandit
# via favicons
# via hyperglass
rlpycairo==0.3.0
# via favicons
ruff==0.2.2
scp==0.14.5
# via netmiko
setuptools==69.1.0
# via gunicorn
# via netmiko
# via nodeenv
# via pytest-dependency
six==1.16.0
# via textfsm
sniffio==1.3.0
# via anyio
# via httpcore
# via httpx
stackprinter==0.2.11
starlette==0.26.1
# via fastapi
stevedore==5.1.0
# via bandit
svglib==1.5.1
# via favicons
taskipy==1.12.2
tenacity==8.2.3
# via netmiko
textfsm==1.1.2
# via netmiko
# via ntc-templates
tinycss2==1.2.1
# via cssselect2
# via svglib
tomli==2.0.1
# via taskipy
typer==0.9.0
# via favicons
# via hyperglass
typing-extensions==4.9.0
# via pydantic
# via typer
uvicorn==0.21.1
# via hyperglass
uvloop==0.17.0
# via hyperglass
virtualenv==20.25.0
# via pre-commit
webencodings==0.5.1
# via cssselect2
# via tinycss2
xmltodict==0.13.0
# via hyperglass

144
requirements.lock Normal file
View File

@@ -0,0 +1,144 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: false
# with-sources: false
-e file:.
aiofiles==23.2.1
# via hyperglass
anyio==4.3.0
# via httpcore
# via starlette
bcrypt==4.1.2
# via paramiko
certifi==2024.2.2
# via httpcore
# via httpx
cffi==1.16.0
# via cryptography
# via pynacl
chardet==5.2.0
# via reportlab
click==8.1.7
# via typer
# via uvicorn
cryptography==42.0.3
# via paramiko
cssselect2==0.7.0
# via svglib
distro==1.8.0
# via hyperglass
fastapi==0.95.1
# via hyperglass
favicons==0.2.2
# via hyperglass
freetype-py==2.4.0
# via rlpycairo
future==0.18.3
# via textfsm
gunicorn==20.1.0
# via hyperglass
h11==0.14.0
# via httpcore
# via uvicorn
httpcore==0.17.3
# via httpx
httpx==0.24.0
# via hyperglass
idna==3.6
# via anyio
# via httpx
loguru==0.7.0
# via hyperglass
lxml==5.1.0
# via svglib
markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
netmiko==4.1.2
# via hyperglass
ntc-templates==4.3.0
# via netmiko
paramiko==3.4.0
# via hyperglass
# via netmiko
# via scp
pillow==10.2.0
# via favicons
# via hyperglass
# via reportlab
psutil==5.9.4
# via hyperglass
py-cpuinfo==9.0.0
# via hyperglass
pycairo==1.26.0
# via rlpycairo
pycparser==2.21
# via cffi
pydantic==1.10.14
# via fastapi
# via hyperglass
pygments==2.17.2
# via rich
pyjwt==2.6.0
# via hyperglass
pynacl==1.5.0
# via paramiko
pyserial==3.5
# via netmiko
pyyaml==6.0.1
# via hyperglass
# via netmiko
redis==4.5.4
# via hyperglass
reportlab==4.1.0
# via favicons
# via svglib
rich==13.7.0
# via favicons
# via hyperglass
rlpycairo==0.3.0
# via favicons
scp==0.14.5
# via netmiko
setuptools==69.1.0
# via gunicorn
# via netmiko
six==1.16.0
# via textfsm
sniffio==1.3.0
# via anyio
# via httpcore
# via httpx
starlette==0.26.1
# via fastapi
svglib==1.5.1
# via favicons
tenacity==8.2.3
# via netmiko
textfsm==1.1.2
# via netmiko
# via ntc-templates
tinycss2==1.2.1
# via cssselect2
# via svglib
typer==0.9.0
# via favicons
# via hyperglass
typing-extensions==4.9.0
# via pydantic
# via typer
uvicorn==0.21.1
# via hyperglass
uvloop==0.17.0
# via hyperglass
webencodings==0.5.1
# via cssselect2
# via tinycss2
xmltodict==0.13.0
# via hyperglass