diff --git a/stubs/gunicorn/METADATA.toml b/stubs/gunicorn/METADATA.toml index b659940f8b15..98ac9a6a255b 100644 --- a/stubs/gunicorn/METADATA.toml +++ b/stubs/gunicorn/METADATA.toml @@ -1,4 +1,4 @@ -version = "25.0.3" +version = "25.1.0" upstream_repository = "https://github.com/benoitc/gunicorn" requires = ["types-gevent"] diff --git a/stubs/gunicorn/gunicorn/config.pyi b/stubs/gunicorn/gunicorn/config.pyi index ba1e706db78e..b5fa328c6ee3 100644 --- a/stubs/gunicorn/gunicorn/config.pyi +++ b/stubs/gunicorn/gunicorn/config.pyi @@ -1283,3 +1283,31 @@ class DirtyWorkerExit(Setting): desc: ClassVar[str] def dirty_worker_exit(arbiter: Arbiter, worker: Worker) -> None: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] + +class ControlSocket(Setting): + name: ClassVar[str] + section: ClassVar[str] + cli: ClassVar[list[str]] + meta: ClassVar[str] + validator: ClassVar[_StringValidatorType] + default: ClassVar[str] + desc: ClassVar[str] + +class ControlSocketMode(Setting): + name: ClassVar[str] + section: ClassVar[str] + cli: ClassVar[list[str]] + meta: ClassVar[str] + validator: ClassVar[_IntValidatorType] + type: ClassVar[Callable[[Any, str], int]] + default: ClassVar[int] + desc: ClassVar[str] + +class ControlSocketDisable(Setting): + name: ClassVar[str] + section: ClassVar[str] + cli: ClassVar[list[str]] + validator: ClassVar[_BoolValidatorType] + action: ClassVar[str] + default: ClassVar[bool] + desc: ClassVar[str] diff --git a/stubs/gunicorn/gunicorn/ctl/__init__.pyi b/stubs/gunicorn/gunicorn/ctl/__init__.pyi new file mode 100644 index 000000000000..03f497fa809f --- /dev/null +++ b/stubs/gunicorn/gunicorn/ctl/__init__.pyi @@ -0,0 +1,5 @@ +from gunicorn.ctl.client import ControlClient as ControlClient +from gunicorn.ctl.protocol import ControlProtocol as ControlProtocol +from gunicorn.ctl.server import ControlSocketServer as ControlSocketServer + +__all__ = ["ControlSocketServer", "ControlClient", "ControlProtocol"] diff --git a/stubs/gunicorn/gunicorn/ctl/cli.pyi b/stubs/gunicorn/gunicorn/ctl/cli.pyi new file mode 100644 index 000000000000..e0e4775c6910 --- /dev/null +++ b/stubs/gunicorn/gunicorn/ctl/cli.pyi @@ -0,0 +1,13 @@ +from _typeshed import Incomplete + +def format_workers(data: dict[str, Incomplete]) -> str: ... +def format_dirty(data: dict[str, Incomplete]) -> str: ... +def format_stats(data: dict[str, Incomplete]) -> str: ... +def format_listeners(data: dict[str, Incomplete]) -> str: ... +def format_config(data: dict[str, Incomplete]) -> str: ... +def format_help(data: dict[str, Incomplete]) -> str: ... +def format_all(data: dict[str, Incomplete]) -> str: ... +def format_response(command: str, data: dict[str, Incomplete]) -> str: ... +def run_command(socket_path: str, command: str, json_output: bool = False) -> int: ... +def run_interactive(socket_path: str, json_output: bool = False) -> int: ... +def main() -> int: ... diff --git a/stubs/gunicorn/gunicorn/ctl/client.pyi b/stubs/gunicorn/gunicorn/ctl/client.pyi new file mode 100644 index 000000000000..7cc5912eed33 --- /dev/null +++ b/stubs/gunicorn/gunicorn/ctl/client.pyi @@ -0,0 +1,16 @@ +from _typeshed import Incomplete, Unused +from typing_extensions import Self + +class ControlClientError(Exception): ... + +class ControlClient: + socket_path: str + timeout: float + def __init__(self, socket_path: str, timeout: float = 30.0) -> None: ... + def connect(self) -> None: ... + def close(self) -> None: ... + def send_command(self, command: str, args: list[str] | None = None) -> dict[Incomplete, Incomplete]: ... + def __enter__(self) -> Self: ... + def __exit__(self, *args: Unused) -> None: ... + +def parse_command(line: str) -> tuple[str, list[str]]: ... diff --git a/stubs/gunicorn/gunicorn/ctl/handlers.pyi b/stubs/gunicorn/gunicorn/ctl/handlers.pyi new file mode 100644 index 000000000000..f70f3d102a6d --- /dev/null +++ b/stubs/gunicorn/gunicorn/ctl/handlers.pyi @@ -0,0 +1,23 @@ +from _typeshed import Incomplete + +from gunicorn.arbiter import Arbiter + +class CommandHandlers: + arbiter: Arbiter + def __init__(self, arbiter: Arbiter) -> None: ... + # TODO: Use TypedDict for next return types + def show_workers(self) -> dict[str, Incomplete]: ... + def show_dirty(self) -> dict[str, Incomplete]: ... + def show_config(self) -> dict[str, Incomplete]: ... + def show_stats(self) -> dict[str, Incomplete]: ... + def show_listeners(self) -> dict[str, Incomplete]: ... + def worker_add(self, count: int = 1) -> dict[str, Incomplete]: ... + def worker_remove(self, count: int = 1) -> dict[str, Incomplete]: ... + def worker_kill(self, pid: int) -> dict[str, Incomplete]: ... + def dirty_add(self, count: int = 1) -> dict[str, Incomplete]: ... + def dirty_remove(self, count: int = 1) -> dict[str, Incomplete]: ... + def reload(self) -> dict[str, Incomplete]: ... + def reopen(self) -> dict[str, Incomplete]: ... + def shutdown(self, mode: str = "graceful") -> dict[str, Incomplete]: ... + def show_all(self) -> dict[str, Incomplete]: ... + def help(self) -> dict[str, Incomplete]: ... diff --git a/stubs/gunicorn/gunicorn/ctl/protocol.pyi b/stubs/gunicorn/gunicorn/ctl/protocol.pyi new file mode 100644 index 000000000000..aa3e3ba92566 --- /dev/null +++ b/stubs/gunicorn/gunicorn/ctl/protocol.pyi @@ -0,0 +1,26 @@ +from _typeshed import Incomplete +from asyncio import StreamReader, StreamWriter +from socket import socket +from typing import ClassVar + +class ProtocolError(Exception): ... + +class ControlProtocol: + MAX_MESSAGE_SIZE: ClassVar[int] + @staticmethod + def encode_message(data: dict[Incomplete, Incomplete]) -> bytes: ... + @staticmethod + def decode_message(data: bytes) -> dict[Incomplete, Incomplete]: ... + @staticmethod + def read_message(sock: socket) -> dict[Incomplete, Incomplete]: ... + @staticmethod + def write_message(sock: socket, data: dict[Incomplete, Incomplete]) -> None: ... + @staticmethod + async def read_message_async(reader: StreamReader) -> dict[Incomplete, Incomplete]: ... + @staticmethod + async def write_message_async(writer: StreamWriter, data: dict[Incomplete, Incomplete]) -> None: ... + +# TODO: Use TypedDict for next return types +def make_request(request_id: int, command: str, args: list[str] | None = None) -> dict[str, Incomplete]: ... +def make_response(request_id: int, data: dict[Incomplete, Incomplete] | None = None) -> dict[str, Incomplete]: ... +def make_error_response(request_id: int, error: str) -> dict[str, Incomplete]: ... diff --git a/stubs/gunicorn/gunicorn/ctl/server.pyi b/stubs/gunicorn/gunicorn/ctl/server.pyi new file mode 100644 index 000000000000..0ab97c7a2df9 --- /dev/null +++ b/stubs/gunicorn/gunicorn/ctl/server.pyi @@ -0,0 +1,11 @@ +from gunicorn.arbiter import Arbiter +from gunicorn.ctl.handlers import CommandHandlers + +class ControlSocketServer: + arbiter: Arbiter + socket_path: str + socket_mode: int + handlers: CommandHandlers + def __init__(self, arbiter: Arbiter, socket_path: str, socket_mode: int = 0o600) -> None: ... + def start(self) -> None: ... + def stop(self) -> None: ... diff --git a/stubs/gunicorn/gunicorn/dirty/__init__.pyi b/stubs/gunicorn/gunicorn/dirty/__init__.pyi index bce1f1dfe24a..9d0294631e6f 100644 --- a/stubs/gunicorn/gunicorn/dirty/__init__.pyi +++ b/stubs/gunicorn/gunicorn/dirty/__init__.pyi @@ -1,3 +1,4 @@ +from . import stash as stash from .app import DirtyApp as DirtyApp from .arbiter import DirtyArbiter as DirtyArbiter from .client import ( @@ -17,6 +18,13 @@ from .errors import ( DirtyTimeoutError as DirtyTimeoutError, DirtyWorkerError as DirtyWorkerError, ) +from .stash import ( + StashClient as StashClient, + StashError as StashError, + StashKeyNotFoundError as StashKeyNotFoundError, + StashTable as StashTable, + StashTableNotFoundError as StashTableNotFoundError, +) __all__ = [ "DirtyError", @@ -32,6 +40,12 @@ __all__ = [ "get_dirty_client_async", "close_dirty_client", "close_dirty_client_async", + "stash", + "StashClient", + "StashTable", + "StashError", + "StashTableNotFoundError", + "StashKeyNotFoundError", "DirtyArbiter", "set_dirty_socket_path", ] diff --git a/stubs/gunicorn/gunicorn/dirty/arbiter.pyi b/stubs/gunicorn/gunicorn/dirty/arbiter.pyi index 250f66db9e1a..c1e9114c29a3 100644 --- a/stubs/gunicorn/gunicorn/dirty/arbiter.pyi +++ b/stubs/gunicorn/gunicorn/dirty/arbiter.pyi @@ -25,17 +25,22 @@ class DirtyArbiter: worker_consumers: dict[int, asyncio.Task[None]] worker_age: int alive: bool + num_workers: int app_specs: dict[str, dict[Incomplete, Incomplete]] app_worker_map: dict[str, set[Incomplete]] worker_app_map: dict[int, list[Incomplete]] + stash_tables: dict[str, dict[Incomplete, Incomplete]] def __init__(self, cfg: Config, log: GLogger, socket_path: str | None = None, pidfile: str | None = None) -> None: ... def run(self) -> None: ... def init_signals(self) -> None: ... async def handle_client(self, reader: StreamReader, writer: StreamWriter) -> None: ... async def route_request(self, request: dict[str, Incomplete], client_writer: StreamWriter) -> None: ... + async def handle_status_request(self, message: dict[str, Incomplete], client_writer: StreamWriter) -> None: ... + async def handle_manage_request(self, message: dict[str, Incomplete], client_writer: StreamWriter) -> None: ... + async def handle_stash_request(self, message: dict[str, Incomplete], client_writer: StreamWriter) -> None: ... async def manage_workers(self) -> None: ... - def spawn_worker(self) -> int | None: ... + def spawn_worker(self, force_all_apps: bool = False) -> int | None: ... def kill_worker(self, pid: int, sig: int) -> None: ... async def murder_workers(self) -> None: ... def reap_workers(self) -> None: ... diff --git a/stubs/gunicorn/gunicorn/dirty/protocol.pyi b/stubs/gunicorn/gunicorn/dirty/protocol.pyi index ef13bf4c7ebd..ce36f88dcd88 100644 --- a/stubs/gunicorn/gunicorn/dirty/protocol.pyi +++ b/stubs/gunicorn/gunicorn/dirty/protocol.pyi @@ -1,10 +1,45 @@ import asyncio import socket from _typeshed import Incomplete -from typing import ClassVar +from typing import ClassVar, Final -class DirtyProtocol: - HEADER_FORMAT: ClassVar[str] +MAGIC: Final = b"GD" +VERSION: Final = 0x01 +MSG_TYPE_REQUEST: Final = 0x01 +MSG_TYPE_RESPONSE: Final = 0x02 +MSG_TYPE_ERROR: Final = 0x03 +MSG_TYPE_CHUNK: Final = 0x04 +MSG_TYPE_END: Final = 0x05 +MSG_TYPE_STASH: Final = 0x10 +MSG_TYPE_STATUS: Final = 0x11 +MSG_TYPE_MANAGE: Final = 0x12 +MSG_TYPE_REQUEST_STR: Final = "request" +MSG_TYPE_RESPONSE_STR: Final = "response" +MSG_TYPE_ERROR_STR: Final = "error" +MSG_TYPE_CHUNK_STR: Final = "chunk" +MSG_TYPE_END_STR: Final = "end" +MSG_TYPE_STASH_STR: Final = "stash" +MSG_TYPE_STATUS_STR: Final = "status" +MSG_TYPE_MANAGE_STR: Final = "manage" +MSG_TYPE_TO_STR: Final[dict[int, str]] +MSG_TYPE_FROM_STR: Final[dict[str, int]] +STASH_OP_PUT: Final = 1 +STASH_OP_GET: Final = 2 +STASH_OP_DELETE: Final = 3 +STASH_OP_KEYS: Final = 4 +STASH_OP_CLEAR: Final = 5 +STASH_OP_INFO: Final = 6 +STASH_OP_ENSURE: Final = 7 +STASH_OP_DELETE_TABLE: Final = 8 +STASH_OP_TABLES: Final = 9 +STASH_OP_EXISTS: Final = 10 +MANAGE_OP_ADD: Final = 1 +MANAGE_OP_REMOVE: Final = 2 +HEADER_FORMAT: Final = ">2sBBIQ" +HEADER_SIZE: Final[int] +MAX_MESSAGE_SIZE: Final = 67108864 + +class BinaryProtocol: HEADER_SIZE: ClassVar[int] MAX_MESSAGE_SIZE: ClassVar[int] MSG_TYPE_REQUEST: ClassVar[str] @@ -12,29 +47,66 @@ class DirtyProtocol: MSG_TYPE_ERROR: ClassVar[str] MSG_TYPE_CHUNK: ClassVar[str] MSG_TYPE_END: ClassVar[str] + MSG_TYPE_STASH: ClassVar[str] + MSG_TYPE_STATUS: ClassVar[str] + MSG_TYPE_MANAGE: ClassVar[str] @staticmethod - def encode(message: dict[Incomplete, Incomplete]) -> bytes: ... + def encode_header(msg_type: int, request_id: int, payload_length: int) -> bytes: ... + @staticmethod + def decode_header(data: bytes) -> tuple[int, int, int]: ... + @staticmethod + def encode_request( + request_id: int, + app_path: str, + action: str, + args: tuple[Incomplete, ...] | None = None, + kwargs: dict[str, Incomplete] | None = None, + ) -> bytes: ... + @staticmethod + def encode_response(request_id: int, result) -> bytes: ... + @staticmethod + def encode_error(request_id: int, error: BaseException | dict[str, Incomplete]) -> bytes: ... + @staticmethod + def encode_chunk(request_id: int, data) -> bytes: ... + @staticmethod + def encode_end(request_id: int) -> bytes: ... @staticmethod - def decode(data: bytes) -> dict[Incomplete, Incomplete]: ... + def encode_status(request_id: int) -> bytes: ... @staticmethod - async def read_message_async(reader: asyncio.StreamReader) -> dict[Incomplete, Incomplete]: ... + def encode_manage(request_id: int, op: int, count: int = 1) -> bytes: ... @staticmethod - async def write_message_async(writer: asyncio.StreamWriter, message: dict[Incomplete, Incomplete]) -> None: ... + def encode_stash(request_id: int, op: int, table: str, key=None, value=None, pattern=None) -> bytes: ... @staticmethod - def read_message(sock: socket.socket) -> dict[Incomplete, Incomplete]: ... + def decode_message(data: bytes) -> tuple[str, int, Incomplete]: ... @staticmethod - def write_message(sock: socket.socket, message: dict[Incomplete, Incomplete]) -> None: ... + async def read_message_async(reader: asyncio.StreamReader) -> dict[str, Incomplete]: ... + @staticmethod + async def write_message_async(writer: asyncio.StreamWriter, message: dict[str, Incomplete]) -> None: ... + @staticmethod + def _recv_exactly(sock: socket.socket, n: int) -> bytes: ... + @staticmethod + def read_message(sock: socket.socket) -> dict[str, Incomplete]: ... + @staticmethod + def write_message(sock: socket.socket, message: dict[str, Incomplete]) -> None: ... + @staticmethod + def _encode_from_dict(message: dict[str, Incomplete]) -> bytes: ... + +DirtyProtocol = BinaryProtocol # TODO: Use TypedDict for results def make_request( - request_id: str, + request_id: int | str, app_path: str, action: str, args: tuple[Incomplete, ...] | None = None, kwargs: dict[str, Incomplete] | None = None, ) -> dict[str, Incomplete]: ... -def make_response(request_id: str, result) -> dict[str, Incomplete]: ... -def make_error_response(request_id: str, error) -> dict[str, Incomplete]: ... -def make_chunk_message(request_id: str, data) -> dict[str, Incomplete]: ... -def make_end_message(request_id: str) -> dict[str, Incomplete]: ... +def make_response(request_id: int | str, result) -> dict[str, Incomplete]: ... +def make_error_response(request_id: int | str, error) -> dict[str, Incomplete]: ... +def make_chunk_message(request_id: int | str, data) -> dict[str, Incomplete]: ... +def make_end_message(request_id: int | str) -> dict[str, Incomplete]: ... +def make_stash_message( + request_id: int | str, op: int, table: str, key=None, value=None, pattern=None +) -> dict[str, Incomplete]: ... +def make_manage_message(request_id: int | str, op: int, count: int = 1) -> dict[str, Incomplete]: ... diff --git a/stubs/gunicorn/gunicorn/dirty/stash.pyi b/stubs/gunicorn/gunicorn/dirty/stash.pyi new file mode 100644 index 000000000000..113180170b71 --- /dev/null +++ b/stubs/gunicorn/gunicorn/dirty/stash.pyi @@ -0,0 +1,71 @@ +from _typeshed import Incomplete +from collections.abc import Iterator +from types import TracebackType +from typing_extensions import Self + +from .errors import DirtyError + +class StashError(DirtyError): ... + +class StashTableNotFoundError(StashError): + table_name: str + + def __init__(self, table_name: str) -> None: ... + +class StashKeyNotFoundError(StashError): + table_name: str + key: str + + def __init__(self, table_name: str, key: str) -> None: ... + +class StashClient: + socket_path: str + timeout: float + + def __init__(self, socket_path: str, timeout: float = 30.0) -> None: ... + def put(self, table: str, key: str, value) -> None: ... + def get(self, table: str, key: str, default=None): ... + def delete(self, table: str, key: str) -> bool: ... + def keys(self, table: str, pattern: str | None = None) -> list[str]: ... + def clear(self, table: str) -> None: ... + def info(self, table: str) -> dict[str, Incomplete]: ... + def ensure(self, table: str) -> None: ... + def exists(self, table: str, key=None) -> bool: ... + def delete_table(self, table: str) -> None: ... + def tables(self) -> list[str]: ... + def table(self, name: str) -> StashTable: ... + def close(self) -> None: ... + def __enter__(self) -> Self: ... + def __exit__( + self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None + ) -> None: ... + +class StashTable: + def __init__(self, client: StashClient, name: str) -> None: ... + @property + def name(self) -> str: ... + def __getitem__(self, key: str): ... + def __setitem__(self, key: str, value) -> None: ... + def __delitem__(self, key: str) -> None: ... + def __contains__(self, key: str) -> bool: ... + def __iter__(self) -> Iterator[str]: ... + def __len__(self) -> int: ... + def get(self, key: str, default=None): ... + def keys(self, pattern: str | None = None) -> list[str]: ... + def clear(self) -> None: ... + def items(self) -> Iterator[tuple[Incomplete, Incomplete]]: ... + def values(self) -> Iterator[Incomplete]: ... + +def set_stash_socket_path(path: str) -> None: ... +def get_stash_socket_path() -> str: ... +def put(table: str, key: str, value) -> None: ... +def get(table: str, key: str, default=None): ... +def delete(table: str, key: str) -> bool: ... +def keys(table: str, pattern: str | None = None) -> list[str]: ... +def clear(table: str) -> None: ... +def info(table: str) -> dict[str, Incomplete]: ... +def ensure(table: str) -> None: ... +def exists(table: str, key: str | None = None) -> bool: ... +def delete_table(table: str) -> None: ... +def tables() -> list[str]: ... +def table(name: str) -> StashTable: ... diff --git a/stubs/gunicorn/gunicorn/dirty/tlv.pyi b/stubs/gunicorn/gunicorn/dirty/tlv.pyi new file mode 100644 index 000000000000..abc9f1432a34 --- /dev/null +++ b/stubs/gunicorn/gunicorn/dirty/tlv.pyi @@ -0,0 +1,25 @@ +from _typeshed import Incomplete +from typing import Final + +TYPE_NONE: Final = 0x00 +TYPE_BOOL: Final = 0x01 +TYPE_INT64: Final = 0x05 +TYPE_FLOAT64: Final = 0x06 +TYPE_BYTES: Final = 0x10 +TYPE_STRING: Final = 0x11 +TYPE_LIST: Final = 0x20 +TYPE_DICT: Final = 0x21 +MAX_STRING_SIZE: Final = 67108864 +MAX_BYTES_SIZE: Final = 67108864 +MAX_LIST_SIZE: Final = 1048576 +MAX_DICT_SIZE: Final = 1048576 + +class TLVEncoder: + @staticmethod + def encode( + value: bool | float | str | bytes | list[Incomplete] | tuple[Incomplete, ...] | dict[object, Incomplete] | None, + ) -> bytes: ... # dict key passed to `str()` function + @staticmethod + def decode(data: bytes, offset: int = 0) -> tuple[Incomplete, int]: ... + @staticmethod + def decode_full(data: bytes): ...