diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..b59f858 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,49 @@ +name: website + +# build the documentation whenever there are new commits on main +on: + push: + branches: + - development + # Alternative: only build for tags. + # tags: + # - '*' + +# security: restrict permissions for CI jobs. +permissions: + contents: read + +jobs: + # Build the documentation and upload the static HTML files as an artifact. + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + + # ADJUST THIS: install all dependencies (including pdoc) + - run: pip install -e . + # ADJUST THIS: build your documentation into docs/. + # We use a custom build script for pdoc itself, ideally you just run `pdoc -o docs/ ...` here. + - run: python docs/make.py + + - uses: actions/upload-pages-artifact@v3 + with: + path: docs/ + + # Deploy the artifact to GitHub pages. + # This is a separate job so that only actions/deploy-pages has the necessary permissions. + deploy: + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..f272e85 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/docs/pyoutlineapi.html b/docs/pyoutlineapi.html new file mode 100644 index 0000000..68f939b --- /dev/null +++ b/docs/pyoutlineapi.html @@ -0,0 +1,2833 @@ + + + + + + + pyoutlineapi API documentation + + + + + + + + + +
+
+

+pyoutlineapi

+ + + + + + +
 1from .client import AsyncOutlineClient, OutlineError, APIError
+ 2from .models import (
+ 3    AccessKey,
+ 4    AccessKeyCreateRequest,
+ 5    AccessKeyList,
+ 6    DataLimit,
+ 7    ErrorResponse,
+ 8    ExperimentalMetrics,
+ 9    MetricsPeriod,
+10    MetricsStatusResponse,
+11    Server,
+12    ServerMetrics,
+13)
+14
+15__version__ = "0.2.0"
+16
+17__all__ = [
+18    "AsyncOutlineClient",
+19    "OutlineError",
+20    "APIError",
+21    "AccessKey",
+22    "AccessKeyCreateRequest",
+23    "AccessKeyList",
+24    "DataLimit",
+25    "ErrorResponse",
+26    "ExperimentalMetrics",
+27    "MetricsPeriod",
+28    "MetricsStatusResponse",
+29    "Server",
+30    "ServerMetrics",
+31]
+
+ + +
+
+ +
+ + class + AsyncOutlineClient: + + + +
+ +
 37class AsyncOutlineClient:
+ 38    """
+ 39    Asynchronous client for the Outline VPN Server API.
+ 40
+ 41    Args:
+ 42        api_url: Base URL for the Outline server API
+ 43        cert_sha256: SHA-256 fingerprint of the server's TLS certificate
+ 44        json_format: Return raw JSON instead of Pydantic models
+ 45        timeout: Request timeout in seconds
+ 46
+ 47    Examples:
+ 48        >>> async def doo_something():
+ 49        ...     async with AsyncOutlineClient(
+ 50        ...         "https://example.com:1234/secret",
+ 51        ...         "ab12cd34..."
+ 52        ...     ) as client:
+ 53        ...         server_info = await client.get_server_info()
+ 54    """
+ 55
+ 56    def __init__(
+ 57        self,
+ 58        api_url: str,
+ 59        cert_sha256: str,
+ 60        *,
+ 61        json_format: bool = True,
+ 62        timeout: float = 30.0,
+ 63    ) -> None:
+ 64        self._api_url = api_url.rstrip("/")
+ 65        self._cert_sha256 = cert_sha256
+ 66        self._json_format = json_format
+ 67        self._timeout = aiohttp.ClientTimeout(total=timeout)
+ 68        self._ssl_context = None
+ 69        self._session: Optional[aiohttp.ClientSession] = None
+ 70        self._in_context = False
+ 71
+ 72    async def __aenter__(self) -> AsyncOutlineClient:
+ 73        """Set up client session for context manager."""
+ 74        self._session = aiohttp.ClientSession(
+ 75            timeout=self._timeout, raise_for_status=True
+ 76        )
+ 77        self._in_context = True
+ 78        return self
+ 79
+ 80    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
+ 81        """Clean up client session."""
+ 82        if self._session:
+ 83            await self._session.close()
+ 84            self._session = None
+ 85        self._in_context = False
+ 86
+ 87    def _ensure_context(self):
+ 88        """Ensure the session context is valid."""
+ 89        if not self._session or self._session.closed:
+ 90            raise RuntimeError("Client session is not initialized or already closed.")
+ 91
+ 92    @overload
+ 93    async def _parse_response(
+ 94        self,
+ 95        response: ClientResponse,
+ 96        model: type[BaseModel],
+ 97        json_format: Literal[True],
+ 98    ) -> JsonDict: ...
+ 99
+100    @overload
+101    async def _parse_response(
+102        self,
+103        response: ClientResponse,
+104        model: type[BaseModel],
+105        json_format: Literal[False],
+106    ) -> BaseModel: ...
+107
+108    @overload
+109    async def _parse_response(
+110        self, response: ClientResponse, model: type[BaseModel], json_format: bool
+111    ) -> Union[JsonDict, BaseModel]: ...
+112
+113    async def _parse_response(
+114        self, response: ClientResponse, model: type[BaseModel], json_format: bool = True
+115    ) -> Union[JsonDict, BaseModel]:
+116        """
+117        Parse and validate API response data.
+118
+119        Args:
+120            response: API response to parse
+121            model: Pydantic model for validation
+122            json_format: Whether to return raw JSON
+123
+124        Returns:
+125            Validated response data
+126
+127        Raises:
+128            ValueError: If response validation fails
+129        """
+130        self._ensure_context()
+131
+132        try:
+133            data = await response.json()
+134        except aiohttp.ContentTypeError:
+135            raise ValueError("Invalid response format") from None
+136        try:
+137            validated = model.model_validate(data)
+138            return validated.model_dump() if json_format else validated
+139        except Exception as e:
+140            raise ValueError(f"Value error: {e}") from e
+141
+142    @staticmethod
+143    async def _handle_error_response(response: ClientResponse) -> None:
+144        """Handle error responses from the API."""
+145        try:
+146            error_data = await response.json()
+147            error = ErrorResponse.model_validate(error_data)
+148            raise APIError(f"{error.code}: {error.message}")
+149        except ValueError:
+150            raise APIError(f"HTTP {response.status}: {response.reason}")
+151
+152    async def _request(
+153        self,
+154        method: str,
+155        endpoint: str,
+156        *,
+157        json: Any = None,
+158        params: Optional[dict[str, Any]] = None,
+159    ) -> Any:
+160        """Make an API request."""
+161        self._ensure_context()
+162
+163        url = self._build_url(endpoint)
+164        ssl_context = self._get_ssl_context()
+165
+166        async with self._session.request(
+167            method,
+168            url,
+169            json=json,
+170            params=params,
+171            ssl=ssl_context,
+172            raise_for_status=False,
+173            timeout=self._timeout,
+174        ) as response:
+175            if response.status >= 400:
+176                await self._handle_error_response(response)
+177
+178            if response.status == 204:
+179                return True  # No content response
+180
+181            try:
+182                await response.json()
+183                return response
+184            except aiohttp.ContentTypeError:
+185                return await response.text()  # Fallback for non-JSON responses
+186            except Exception as e:
+187                raise APIError(f"Failed to parse response from {url}: {e}") from e
+188
+189    def _build_url(self, endpoint: str) -> str:
+190        """Build and validate the full URL for the API request."""
+191        if not isinstance(endpoint, str):
+192            raise ValueError("Endpoint must be a string")
+193
+194        endpoint = endpoint.lstrip("/")
+195        url = f"{self._api_url}/{endpoint}"
+196
+197        parsed_url = urlparse(url)
+198        if not parsed_url.scheme or not parsed_url.netloc:
+199            raise ValueError(f"Invalid URL: {url}")
+200
+201        return url
+202
+203    def _get_ssl_context(self) -> Optional[Fingerprint]:
+204        """Create an SSL context if a certificate fingerprint is provided."""
+205        if not self._cert_sha256:
+206            return None
+207
+208        try:
+209            fingerprint = binascii.unhexlify(self._cert_sha256)
+210            return Fingerprint(fingerprint)
+211        except binascii.Error as e:
+212            raise ValueError(f"Invalid certificate SHA256: {self._cert_sha256}") from e
+213        except Exception as e:
+214            raise OutlineError("Error while creating SSL context") from e
+215
+216    async def get_server_info(self) -> Union[JsonDict, Server]:
+217        """
+218        Get server information.
+219
+220        Returns:
+221            Server information including name, ID, and configuration.
+222
+223        Examples:
+224            >>> async def doo_something():
+225            ...     async with AsyncOutlineClient(
+226            ...         "https://example.com:1234/secret",
+227            ...         "ab12cd34..."
+228            ...     ) as client:
+229            ...         server = await client.get_server_info()
+230            ...         print(f"Server {server.name} running version {server.version}")
+231        """
+232        response = await self._request("GET", "server")
+233        return await self._parse_response(
+234            response, Server, json_format=self._json_format
+235        )
+236
+237    async def rename_server(self, name: str) -> bool:
+238        """
+239        Rename the server.
+240
+241        Args:
+242            name: New server name
+243
+244        Returns:
+245            True if successful
+246
+247        Examples:
+248            >>> async def doo_something():
+249            ...     async with AsyncOutlineClient(
+250            ...         "https://example.com:1234/secret",
+251            ...         "ab12cd34..."
+252            ...     ) as client:
+253            ...     success = await client.rename_server("My VPN Server")
+254            ...     if success:
+255            ...         print("Server renamed successfully")
+256        """
+257        return await self._request("PUT", "name", json={"name": name})
+258
+259    async def set_hostname(self, hostname: str) -> bool:
+260        """
+261        Set server hostname for access keys.
+262
+263        Args:
+264            hostname: New hostname or IP address
+265
+266        Returns:
+267            True if successful
+268
+269        Raises:
+270            APIError: If hostname is invalid
+271
+272        Examples:
+273            >>> async def doo_something():
+274            ...     async with AsyncOutlineClient(
+275            ...         "https://example.com:1234/secret",
+276            ...         "ab12cd34..."
+277            ...     ) as client:
+278            ...         await client.set_hostname("vpn.example.com")
+279            ...         # Or use IP address
+280            ...         await client.set_hostname("203.0.113.1")
+281        """
+282        return await self._request(
+283            "PUT", "server/hostname-for-access-keys", json={"hostname": hostname}
+284        )
+285
+286    async def set_default_port(self, port: int) -> bool:
+287        """
+288        Set default port for new access keys.
+289
+290        Args:
+291            port: Port number (1025-65535)
+292
+293        Returns:
+294            True if successful
+295
+296        Raises:
+297            APIError: If port is invalid or in use
+298
+299        Examples:
+300            >>> async def doo_something():
+301            ...     async with AsyncOutlineClient(
+302            ...         "https://example.com:1234/secret",
+303            ...         "ab12cd34..."
+304            ...     ) as client:
+305            ...         await client.set_default_port(8388)
+306
+307        """
+308        return await self._request(
+309            "PUT", "server/port-for-new-access-keys", json={"port": port}
+310        )
+311
+312    async def get_metrics_status(self) -> dict[str, Any] | BaseModel:
+313        """
+314        Get whether metrics collection is enabled.
+315
+316        Returns:
+317            Current metrics collection status
+318
+319        Examples:
+320            >>> async def doo_something():
+321            ...     async with AsyncOutlineClient(
+322            ...         "https://example.com:1234/secret",
+323            ...         "ab12cd34..."
+324            ...     ) as client:
+325            ...         if await client.get_metrics_status():
+326            ...             print("Metrics collection is enabled")
+327        """
+328        response = await self._request("GET", "metrics/enabled")
+329        data = await self._parse_response(
+330            response, MetricsStatusResponse, json_format=self._json_format
+331        )
+332        return data
+333
+334    async def set_metrics_status(self, enabled: bool) -> bool:
+335        """
+336        Enable or disable metrics collection.
+337
+338        Args:
+339            enabled: Whether to enable metrics
+340
+341        Returns:
+342            True if successful
+343
+344        Examples:
+345            >>> async def doo_something():
+346            ...     async with AsyncOutlineClient(
+347            ...         "https://example.com:1234/secret",
+348            ...         "ab12cd34..."
+349            ...     ) as client:
+350            ...         # Enable metrics
+351            ...         await client.set_metrics_status(True)
+352            ...         # Check new status
+353            ...         is_enabled = await client.get_metrics_status()
+354        """
+355        return await self._request(
+356            "PUT", "metrics/enabled", json={"metricsEnabled": enabled}
+357        )
+358
+359    async def get_transfer_metrics(
+360        self, period: MetricsPeriod = MetricsPeriod.MONTHLY
+361    ) -> Union[JsonDict, ServerMetrics]:
+362        """
+363        Get transfer metrics for specified period.
+364
+365        Args:
+366            period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
+367
+368        Returns:
+369            Transfer metrics data for each access key
+370
+371        Examples:
+372            >>> async def doo_something():
+373            ...     async with AsyncOutlineClient(
+374            ...         "https://example.com:1234/secret",
+375            ...         "ab12cd34..."
+376            ...     ) as client:
+377            ...         # Get monthly metrics
+378            ...         metrics = await client.get_transfer_metrics()
+379            ...         # Or get daily metrics
+380            ...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)
+381            ...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():
+382            ...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")
+383        """
+384        response = await self._request(
+385            "GET", "metrics/transfer", params={"period": period.value}
+386        )
+387        return await self._parse_response(
+388            response, ServerMetrics, json_format=self._json_format
+389        )
+390
+391    async def create_access_key(
+392        self,
+393        *,
+394        name: Optional[str] = None,
+395        password: Optional[str] = None,
+396        port: Optional[int] = None,
+397        method: Optional[str] = None,
+398        limit: Optional[DataLimit] = None,
+399    ) -> Union[JsonDict, AccessKey]:
+400        """
+401        Create a new access key.
+402
+403        Args:
+404            name: Optional key name
+405            password: Optional password
+406            port: Optional port number (1-65535)
+407            method: Optional encryption method
+408            limit: Optional data transfer limit
+409
+410        Returns:
+411            New access key details
+412
+413        Examples:
+414            >>> async def doo_something():
+415            ...     async with AsyncOutlineClient(
+416            ...         "https://example.com:1234/secret",
+417            ...         "ab12cd34..."
+418            ...     ) as client:
+419            ...         # Create basic key
+420            ...         key = await client.create_access_key(name="User 1")
+421            ...
+422            ...         # Create key with data limit
+423            ...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB
+424            ...         key = await client.create_access_key(
+425            ...             name="Limited User",
+426            ...             port=8388,
+427            ...             limit=_limit
+428            ...         )
+429            ...         print(f"Created key: {key.access_url}")
+430        """
+431        request = AccessKeyCreateRequest(
+432            name=name, password=password, port=port, method=method, limit=limit
+433        )
+434        response = await self._request(
+435            "POST", "access-keys", json=request.model_dump(exclude_none=True)
+436        )
+437        return await self._parse_response(
+438            response, AccessKey, json_format=self._json_format
+439        )
+440
+441    async def get_access_keys(self) -> Union[JsonDict, AccessKeyList]:
+442        """
+443        Get all access keys.
+444
+445        Returns:
+446            List of all access keys
+447
+448        Examples:
+449            >>> async def doo_something():
+450            ...     async with AsyncOutlineClient(
+451            ...         "https://example.com:1234/secret",
+452            ...         "ab12cd34..."
+453            ...     ) as client:
+454            ...         keys = await client.get_access_keys()
+455            ...         for key in keys.access_keys:
+456            ...             print(f"Key {key.id}: {key.name or 'unnamed'}")
+457            ...             if key.data_limit:
+458            ...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")
+459        """
+460        response = await self._request("GET", "access-keys")
+461        return await self._parse_response(
+462            response, AccessKeyList, json_format=self._json_format
+463        )
+464
+465    async def get_access_key(self, key_id: int) -> Union[JsonDict, AccessKey]:
+466        """
+467        Get specific access key.
+468
+469        Args:
+470            key_id: Access key ID
+471
+472        Returns:
+473            Access key details
+474
+475        Raises:
+476            APIError: If key doesn't exist
+477
+478        Examples:
+479            >>> async def doo_something():
+480            ...     async with AsyncOutlineClient(
+481            ...         "https://example.com:1234/secret",
+482            ...         "ab12cd34..."
+483            ...     ) as client:
+484            ...         key = await client.get_access_key(1)
+485            ...         print(f"Port: {key.port}")
+486            ...         print(f"URL: {key.access_url}")
+487        """
+488        response = await self._request("GET", f"access-keys/{key_id}")
+489        return await self._parse_response(
+490            response, AccessKey, json_format=self._json_format
+491        )
+492
+493    async def rename_access_key(self, key_id: int, name: str) -> bool:
+494        """
+495        Rename access key.
+496
+497        Args:
+498            key_id: Access key ID
+499            name: New name
+500
+501        Returns:
+502            True if successful
+503
+504        Raises:
+505            APIError: If key doesn't exist
+506
+507        Examples:
+508            >>> async def doo_something():
+509            ...     async with AsyncOutlineClient(
+510            ...         "https://example.com:1234/secret",
+511            ...         "ab12cd34..."
+512            ...     ) as client:
+513            ...         # Rename key
+514            ...         await client.rename_access_key(1, "Alice")
+515            ...
+516            ...         # Verify new name
+517            ...         key = await client.get_access_key(1)
+518            ...         assert key.name == "Alice"
+519        """
+520        return await self._request(
+521            "PUT", f"access-keys/{key_id}/name", json={"name": name}
+522        )
+523
+524    async def delete_access_key(self, key_id: int) -> bool:
+525        """
+526        Delete access key.
+527
+528        Args:
+529            key_id: Access key ID
+530
+531        Returns:
+532            True if successful
+533
+534        Raises:
+535            APIError: If key doesn't exist
+536
+537        Examples:
+538            >>> async def doo_something():
+539            ...     async with AsyncOutlineClient(
+540            ...         "https://example.com:1234/secret",
+541            ...         "ab12cd34..."
+542            ...     ) as client:
+543            ...         if await client.delete_access_key(1):
+544            ...             print("Key deleted")
+545
+546        """
+547        return await self._request("DELETE", f"access-keys/{key_id}")
+548
+549    async def set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool:
+550        """
+551        Set data transfer limit for access key.
+552
+553        Args:
+554            key_id: Access key ID
+555            bytes_limit: Limit in bytes (must be positive)
+556
+557        Returns:
+558            True if successful
+559
+560        Raises:
+561            APIError: If key doesn't exist or limit is invalid
+562
+563        Examples:
+564            >>> async def doo_something():
+565            ...     async with AsyncOutlineClient(
+566            ...         "https://example.com:1234/secret",
+567            ...         "ab12cd34..."
+568            ...     ) as client:
+569            ...         # Set 5 GB limit
+570            ...         limit = 5 * 1024**3  # 5 GB in bytes
+571            ...         await client.set_access_key_data_limit(1, limit)
+572            ...
+573            ...         # Verify limit
+574            ...         key = await client.get_access_key(1)
+575            ...         assert key.data_limit and key.data_limit.bytes == limit
+576        """
+577        return await self._request(
+578            "PUT",
+579            f"access-keys/{key_id}/data-limit",
+580            json={"limit": {"bytes": bytes_limit}},
+581        )
+582
+583    async def remove_access_key_data_limit(self, key_id: str) -> bool:
+584        """
+585        Remove data transfer limit from access key.
+586
+587        Args:
+588            key_id: Access key ID
+589
+590        Returns:
+591            True if successful
+592
+593        Raises:
+594            APIError: If key doesn't exist
+595        """
+596        return await self._request("DELETE", f"access-keys/{key_id}/data-limit")
+
+ + +

Asynchronous client for the Outline VPN Server API.

+ +
Arguments:
+ +
    +
  • api_url: Base URL for the Outline server API
  • +
  • cert_sha256: SHA-256 fingerprint of the server's TLS certificate
  • +
  • json_format: Return raw JSON instead of Pydantic models
  • +
  • timeout: Request timeout in seconds
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         server_info = await client.get_server_info()
+
+
+
+
+ + +
+ +
+ + AsyncOutlineClient( api_url: str, cert_sha256: str, *, json_format: bool = True, timeout: float = 30.0) + + + +
+ +
56    def __init__(
+57        self,
+58        api_url: str,
+59        cert_sha256: str,
+60        *,
+61        json_format: bool = True,
+62        timeout: float = 30.0,
+63    ) -> None:
+64        self._api_url = api_url.rstrip("/")
+65        self._cert_sha256 = cert_sha256
+66        self._json_format = json_format
+67        self._timeout = aiohttp.ClientTimeout(total=timeout)
+68        self._ssl_context = None
+69        self._session: Optional[aiohttp.ClientSession] = None
+70        self._in_context = False
+
+ + + + +
+
+ +
+ + async def + get_server_info(self) -> Union[dict[str, Any], Server]: + + + +
+ +
216    async def get_server_info(self) -> Union[JsonDict, Server]:
+217        """
+218        Get server information.
+219
+220        Returns:
+221            Server information including name, ID, and configuration.
+222
+223        Examples:
+224            >>> async def doo_something():
+225            ...     async with AsyncOutlineClient(
+226            ...         "https://example.com:1234/secret",
+227            ...         "ab12cd34..."
+228            ...     ) as client:
+229            ...         server = await client.get_server_info()
+230            ...         print(f"Server {server.name} running version {server.version}")
+231        """
+232        response = await self._request("GET", "server")
+233        return await self._parse_response(
+234            response, Server, json_format=self._json_format
+235        )
+
+ + +

Get server information.

+ +
Returns:
+ +
+

Server information including name, ID, and configuration.

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         server = await client.get_server_info()
+...         print(f"Server {server.name} running version {server.version}")
+
+
+
+
+ + +
+
+ +
+ + async def + rename_server(self, name: str) -> bool: + + + +
+ +
237    async def rename_server(self, name: str) -> bool:
+238        """
+239        Rename the server.
+240
+241        Args:
+242            name: New server name
+243
+244        Returns:
+245            True if successful
+246
+247        Examples:
+248            >>> async def doo_something():
+249            ...     async with AsyncOutlineClient(
+250            ...         "https://example.com:1234/secret",
+251            ...         "ab12cd34..."
+252            ...     ) as client:
+253            ...     success = await client.rename_server("My VPN Server")
+254            ...     if success:
+255            ...         print("Server renamed successfully")
+256        """
+257        return await self._request("PUT", "name", json={"name": name})
+
+ + +

Rename the server.

+ +
Arguments:
+ +
    +
  • name: New server name
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...     success = await client.rename_server("My VPN Server")
+...     if success:
+...         print("Server renamed successfully")
+
+
+
+
+ + +
+
+ +
+ + async def + set_hostname(self, hostname: str) -> bool: + + + +
+ +
259    async def set_hostname(self, hostname: str) -> bool:
+260        """
+261        Set server hostname for access keys.
+262
+263        Args:
+264            hostname: New hostname or IP address
+265
+266        Returns:
+267            True if successful
+268
+269        Raises:
+270            APIError: If hostname is invalid
+271
+272        Examples:
+273            >>> async def doo_something():
+274            ...     async with AsyncOutlineClient(
+275            ...         "https://example.com:1234/secret",
+276            ...         "ab12cd34..."
+277            ...     ) as client:
+278            ...         await client.set_hostname("vpn.example.com")
+279            ...         # Or use IP address
+280            ...         await client.set_hostname("203.0.113.1")
+281        """
+282        return await self._request(
+283            "PUT", "server/hostname-for-access-keys", json={"hostname": hostname}
+284        )
+
+ + +

Set server hostname for access keys.

+ +
Arguments:
+ +
    +
  • hostname: New hostname or IP address
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Raises:
+ +
    +
  • APIError: If hostname is invalid
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         await client.set_hostname("vpn.example.com")
+...         # Or use IP address
+...         await client.set_hostname("203.0.113.1")
+
+
+
+
+ + +
+
+ +
+ + async def + set_default_port(self, port: int) -> bool: + + + +
+ +
286    async def set_default_port(self, port: int) -> bool:
+287        """
+288        Set default port for new access keys.
+289
+290        Args:
+291            port: Port number (1025-65535)
+292
+293        Returns:
+294            True if successful
+295
+296        Raises:
+297            APIError: If port is invalid or in use
+298
+299        Examples:
+300            >>> async def doo_something():
+301            ...     async with AsyncOutlineClient(
+302            ...         "https://example.com:1234/secret",
+303            ...         "ab12cd34..."
+304            ...     ) as client:
+305            ...         await client.set_default_port(8388)
+306
+307        """
+308        return await self._request(
+309            "PUT", "server/port-for-new-access-keys", json={"port": port}
+310        )
+
+ + +

Set default port for new access keys.

+ +
Arguments:
+ +
    +
  • port: Port number (1025-65535)
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Raises:
+ +
    +
  • APIError: If port is invalid or in use
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         await client.set_default_port(8388)
+
+
+
+
+ + +
+
+ +
+ + async def + get_metrics_status(self) -> dict[str, typing.Any] | pydantic.main.BaseModel: + + + +
+ +
312    async def get_metrics_status(self) -> dict[str, Any] | BaseModel:
+313        """
+314        Get whether metrics collection is enabled.
+315
+316        Returns:
+317            Current metrics collection status
+318
+319        Examples:
+320            >>> async def doo_something():
+321            ...     async with AsyncOutlineClient(
+322            ...         "https://example.com:1234/secret",
+323            ...         "ab12cd34..."
+324            ...     ) as client:
+325            ...         if await client.get_metrics_status():
+326            ...             print("Metrics collection is enabled")
+327        """
+328        response = await self._request("GET", "metrics/enabled")
+329        data = await self._parse_response(
+330            response, MetricsStatusResponse, json_format=self._json_format
+331        )
+332        return data
+
+ + +

Get whether metrics collection is enabled.

+ +
Returns:
+ +
+

Current metrics collection status

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         if await client.get_metrics_status():
+...             print("Metrics collection is enabled")
+
+
+
+
+ + +
+
+ +
+ + async def + set_metrics_status(self, enabled: bool) -> bool: + + + +
+ +
334    async def set_metrics_status(self, enabled: bool) -> bool:
+335        """
+336        Enable or disable metrics collection.
+337
+338        Args:
+339            enabled: Whether to enable metrics
+340
+341        Returns:
+342            True if successful
+343
+344        Examples:
+345            >>> async def doo_something():
+346            ...     async with AsyncOutlineClient(
+347            ...         "https://example.com:1234/secret",
+348            ...         "ab12cd34..."
+349            ...     ) as client:
+350            ...         # Enable metrics
+351            ...         await client.set_metrics_status(True)
+352            ...         # Check new status
+353            ...         is_enabled = await client.get_metrics_status()
+354        """
+355        return await self._request(
+356            "PUT", "metrics/enabled", json={"metricsEnabled": enabled}
+357        )
+
+ + +

Enable or disable metrics collection.

+ +
Arguments:
+ +
    +
  • enabled: Whether to enable metrics
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         # Enable metrics
+...         await client.set_metrics_status(True)
+...         # Check new status
+...         is_enabled = await client.get_metrics_status()
+
+
+
+
+ + +
+
+ +
+ + async def + get_transfer_metrics( self, period: MetricsPeriod = <MetricsPeriod.MONTHLY: 'monthly'>) -> Union[dict[str, Any], ServerMetrics]: + + + +
+ +
359    async def get_transfer_metrics(
+360        self, period: MetricsPeriod = MetricsPeriod.MONTHLY
+361    ) -> Union[JsonDict, ServerMetrics]:
+362        """
+363        Get transfer metrics for specified period.
+364
+365        Args:
+366            period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
+367
+368        Returns:
+369            Transfer metrics data for each access key
+370
+371        Examples:
+372            >>> async def doo_something():
+373            ...     async with AsyncOutlineClient(
+374            ...         "https://example.com:1234/secret",
+375            ...         "ab12cd34..."
+376            ...     ) as client:
+377            ...         # Get monthly metrics
+378            ...         metrics = await client.get_transfer_metrics()
+379            ...         # Or get daily metrics
+380            ...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)
+381            ...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():
+382            ...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")
+383        """
+384        response = await self._request(
+385            "GET", "metrics/transfer", params={"period": period.value}
+386        )
+387        return await self._parse_response(
+388            response, ServerMetrics, json_format=self._json_format
+389        )
+
+ + +

Get transfer metrics for specified period.

+ +
Arguments:
+ +
    +
  • period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
  • +
+ +
Returns:
+ +
+

Transfer metrics data for each access key

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         # Get monthly metrics
+...         metrics = await client.get_transfer_metrics()
+...         # Or get daily metrics
+...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)
+...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():
+...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")
+
+
+
+
+ + +
+
+ +
+ + async def + create_access_key( self, *, name: Optional[str] = None, password: Optional[str] = None, port: Optional[int] = None, method: Optional[str] = None, limit: Optional[DataLimit] = None) -> Union[dict[str, Any], AccessKey]: + + + +
+ +
391    async def create_access_key(
+392        self,
+393        *,
+394        name: Optional[str] = None,
+395        password: Optional[str] = None,
+396        port: Optional[int] = None,
+397        method: Optional[str] = None,
+398        limit: Optional[DataLimit] = None,
+399    ) -> Union[JsonDict, AccessKey]:
+400        """
+401        Create a new access key.
+402
+403        Args:
+404            name: Optional key name
+405            password: Optional password
+406            port: Optional port number (1-65535)
+407            method: Optional encryption method
+408            limit: Optional data transfer limit
+409
+410        Returns:
+411            New access key details
+412
+413        Examples:
+414            >>> async def doo_something():
+415            ...     async with AsyncOutlineClient(
+416            ...         "https://example.com:1234/secret",
+417            ...         "ab12cd34..."
+418            ...     ) as client:
+419            ...         # Create basic key
+420            ...         key = await client.create_access_key(name="User 1")
+421            ...
+422            ...         # Create key with data limit
+423            ...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB
+424            ...         key = await client.create_access_key(
+425            ...             name="Limited User",
+426            ...             port=8388,
+427            ...             limit=_limit
+428            ...         )
+429            ...         print(f"Created key: {key.access_url}")
+430        """
+431        request = AccessKeyCreateRequest(
+432            name=name, password=password, port=port, method=method, limit=limit
+433        )
+434        response = await self._request(
+435            "POST", "access-keys", json=request.model_dump(exclude_none=True)
+436        )
+437        return await self._parse_response(
+438            response, AccessKey, json_format=self._json_format
+439        )
+
+ + +

Create a new access key.

+ +
Arguments:
+ +
    +
  • name: Optional key name
  • +
  • password: Optional password
  • +
  • port: Optional port number (1-65535)
  • +
  • method: Optional encryption method
  • +
  • limit: Optional data transfer limit
  • +
+ +
Returns:
+ +
+

New access key details

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         # Create basic key
+...         key = await client.create_access_key(name="User 1")
+...
+...         # Create key with data limit
+...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB
+...         key = await client.create_access_key(
+...             name="Limited User",
+...             port=8388,
+...             limit=_limit
+...         )
+...         print(f"Created key: {key.access_url}")
+
+
+
+
+ + +
+
+ +
+ + async def + get_access_keys(self) -> Union[dict[str, Any], AccessKeyList]: + + + +
+ +
441    async def get_access_keys(self) -> Union[JsonDict, AccessKeyList]:
+442        """
+443        Get all access keys.
+444
+445        Returns:
+446            List of all access keys
+447
+448        Examples:
+449            >>> async def doo_something():
+450            ...     async with AsyncOutlineClient(
+451            ...         "https://example.com:1234/secret",
+452            ...         "ab12cd34..."
+453            ...     ) as client:
+454            ...         keys = await client.get_access_keys()
+455            ...         for key in keys.access_keys:
+456            ...             print(f"Key {key.id}: {key.name or 'unnamed'}")
+457            ...             if key.data_limit:
+458            ...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")
+459        """
+460        response = await self._request("GET", "access-keys")
+461        return await self._parse_response(
+462            response, AccessKeyList, json_format=self._json_format
+463        )
+
+ + +

Get all access keys.

+ +
Returns:
+ +
+

List of all access keys

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         keys = await client.get_access_keys()
+...         for key in keys.access_keys:
+...             print(f"Key {key.id}: {key.name or 'unnamed'}")
+...             if key.data_limit:
+...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")
+
+
+
+
+ + +
+
+ +
+ + async def + get_access_key( self, key_id: int) -> Union[dict[str, Any], AccessKey]: + + + +
+ +
465    async def get_access_key(self, key_id: int) -> Union[JsonDict, AccessKey]:
+466        """
+467        Get specific access key.
+468
+469        Args:
+470            key_id: Access key ID
+471
+472        Returns:
+473            Access key details
+474
+475        Raises:
+476            APIError: If key doesn't exist
+477
+478        Examples:
+479            >>> async def doo_something():
+480            ...     async with AsyncOutlineClient(
+481            ...         "https://example.com:1234/secret",
+482            ...         "ab12cd34..."
+483            ...     ) as client:
+484            ...         key = await client.get_access_key(1)
+485            ...         print(f"Port: {key.port}")
+486            ...         print(f"URL: {key.access_url}")
+487        """
+488        response = await self._request("GET", f"access-keys/{key_id}")
+489        return await self._parse_response(
+490            response, AccessKey, json_format=self._json_format
+491        )
+
+ + +

Get specific access key.

+ +
Arguments:
+ +
    +
  • key_id: Access key ID
  • +
+ +
Returns:
+ +
+

Access key details

+
+ +
Raises:
+ +
    +
  • APIError: If key doesn't exist
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         key = await client.get_access_key(1)
+...         print(f"Port: {key.port}")
+...         print(f"URL: {key.access_url}")
+
+
+
+
+ + +
+
+ +
+ + async def + rename_access_key(self, key_id: int, name: str) -> bool: + + + +
+ +
493    async def rename_access_key(self, key_id: int, name: str) -> bool:
+494        """
+495        Rename access key.
+496
+497        Args:
+498            key_id: Access key ID
+499            name: New name
+500
+501        Returns:
+502            True if successful
+503
+504        Raises:
+505            APIError: If key doesn't exist
+506
+507        Examples:
+508            >>> async def doo_something():
+509            ...     async with AsyncOutlineClient(
+510            ...         "https://example.com:1234/secret",
+511            ...         "ab12cd34..."
+512            ...     ) as client:
+513            ...         # Rename key
+514            ...         await client.rename_access_key(1, "Alice")
+515            ...
+516            ...         # Verify new name
+517            ...         key = await client.get_access_key(1)
+518            ...         assert key.name == "Alice"
+519        """
+520        return await self._request(
+521            "PUT", f"access-keys/{key_id}/name", json={"name": name}
+522        )
+
+ + +

Rename access key.

+ +
Arguments:
+ +
    +
  • key_id: Access key ID
  • +
  • name: New name
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Raises:
+ +
    +
  • APIError: If key doesn't exist
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         # Rename key
+...         await client.rename_access_key(1, "Alice")
+...
+...         # Verify new name
+...         key = await client.get_access_key(1)
+...         assert key.name == "Alice"
+
+
+
+
+ + +
+
+ +
+ + async def + delete_access_key(self, key_id: int) -> bool: + + + +
+ +
524    async def delete_access_key(self, key_id: int) -> bool:
+525        """
+526        Delete access key.
+527
+528        Args:
+529            key_id: Access key ID
+530
+531        Returns:
+532            True if successful
+533
+534        Raises:
+535            APIError: If key doesn't exist
+536
+537        Examples:
+538            >>> async def doo_something():
+539            ...     async with AsyncOutlineClient(
+540            ...         "https://example.com:1234/secret",
+541            ...         "ab12cd34..."
+542            ...     ) as client:
+543            ...         if await client.delete_access_key(1):
+544            ...             print("Key deleted")
+545
+546        """
+547        return await self._request("DELETE", f"access-keys/{key_id}")
+
+ + +

Delete access key.

+ +
Arguments:
+ +
    +
  • key_id: Access key ID
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Raises:
+ +
    +
  • APIError: If key doesn't exist
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         if await client.delete_access_key(1):
+...             print("Key deleted")
+
+
+
+
+ + +
+
+ +
+ + async def + set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool: + + + +
+ +
549    async def set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool:
+550        """
+551        Set data transfer limit for access key.
+552
+553        Args:
+554            key_id: Access key ID
+555            bytes_limit: Limit in bytes (must be positive)
+556
+557        Returns:
+558            True if successful
+559
+560        Raises:
+561            APIError: If key doesn't exist or limit is invalid
+562
+563        Examples:
+564            >>> async def doo_something():
+565            ...     async with AsyncOutlineClient(
+566            ...         "https://example.com:1234/secret",
+567            ...         "ab12cd34..."
+568            ...     ) as client:
+569            ...         # Set 5 GB limit
+570            ...         limit = 5 * 1024**3  # 5 GB in bytes
+571            ...         await client.set_access_key_data_limit(1, limit)
+572            ...
+573            ...         # Verify limit
+574            ...         key = await client.get_access_key(1)
+575            ...         assert key.data_limit and key.data_limit.bytes == limit
+576        """
+577        return await self._request(
+578            "PUT",
+579            f"access-keys/{key_id}/data-limit",
+580            json={"limit": {"bytes": bytes_limit}},
+581        )
+
+ + +

Set data transfer limit for access key.

+ +
Arguments:
+ +
    +
  • key_id: Access key ID
  • +
  • bytes_limit: Limit in bytes (must be positive)
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Raises:
+ +
    +
  • APIError: If key doesn't exist or limit is invalid
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         # Set 5 GB limit
+...         limit = 5 * 1024**3  # 5 GB in bytes
+...         await client.set_access_key_data_limit(1, limit)
+...
+...         # Verify limit
+...         key = await client.get_access_key(1)
+...         assert key.data_limit and key.data_limit.bytes == limit
+
+
+
+
+ + +
+
+ +
+ + async def + remove_access_key_data_limit(self, key_id: str) -> bool: + + + +
+ +
583    async def remove_access_key_data_limit(self, key_id: str) -> bool:
+584        """
+585        Remove data transfer limit from access key.
+586
+587        Args:
+588            key_id: Access key ID
+589
+590        Returns:
+591            True if successful
+592
+593        Raises:
+594            APIError: If key doesn't exist
+595        """
+596        return await self._request("DELETE", f"access-keys/{key_id}/data-limit")
+
+ + +

Remove data transfer limit from access key.

+ +
Arguments:
+ +
    +
  • key_id: Access key ID
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Raises:
+ +
    +
  • APIError: If key doesn't exist
  • +
+
+ + +
+
+
+ +
+ + class + OutlineError(builtins.Exception): + + + +
+ +
25class OutlineError(Exception):
+26    """Base exception for Outline client errors."""
+
+ + +

Base exception for Outline client errors.

+
+ + +
+
+ +
+ + class + APIError(pyoutlineapi.OutlineError): + + + +
+ +
29class APIError(OutlineError):
+30    """Raised when API requests fail."""
+
+ + +

Raised when API requests fail.

+
+ + +
+
+ +
+ + class + AccessKey(pydantic.main.BaseModel): + + + +
+ +
28class AccessKey(BaseModel):
+29    """Access key details."""
+30
+31    id: int
+32    name: Optional[str] = None
+33    password: str
+34    port: int = Field(gt=0, lt=65536)
+35    method: str
+36    access_url: str = Field(alias="accessUrl")
+37    data_limit: Optional[DataLimit] = Field(None, alias="dataLimit")
+
+ + +

Access key details.

+
+ + +
+
+ id: int + + +
+ + + + +
+
+
+ name: Optional[str] + + +
+ + + + +
+
+
+ password: str + + +
+ + + + +
+
+
+ port: int + + +
+ + + + +
+
+
+ method: str + + +
+ + + + +
+
+
+ access_url: str + + +
+ + + + +
+
+
+ data_limit: Optional[DataLimit] + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + AccessKeyCreateRequest(pydantic.main.BaseModel): + + + +
+ +
105class AccessKeyCreateRequest(BaseModel):
+106    """
+107    Request parameters for creating an access key.
+108    Per OpenAPI: /access-keys POST request body
+109    """
+110
+111    name: Optional[str] = None
+112    method: Optional[str] = None
+113    password: Optional[str] = None
+114    port: Optional[int] = Field(None, gt=0, lt=65536)
+115    limit: Optional[DataLimit] = None
+
+ + +

Request parameters for creating an access key. +Per OpenAPI: /access-keys POST request body

+
+ + +
+
+ name: Optional[str] + + +
+ + + + +
+
+
+ method: Optional[str] + + +
+ + + + +
+
+
+ password: Optional[str] + + +
+ + + + +
+
+
+ port: Optional[int] + + +
+ + + + +
+
+
+ limit: Optional[DataLimit] + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + AccessKeyList(pydantic.main.BaseModel): + + + +
+ +
40class AccessKeyList(BaseModel):
+41    """List of access keys."""
+42
+43    access_keys: list[AccessKey] = Field(alias="accessKeys")
+
+ + +

List of access keys.

+
+ + +
+
+ access_keys: list[AccessKey] + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + DataLimit(pydantic.main.BaseModel): + + + +
+ +
16class DataLimit(BaseModel):
+17    """Data transfer limit configuration."""
+18
+19    bytes: int = Field(gt=0)
+20
+21    @field_validator("bytes")
+22    def validate_bytes(cls, v: int) -> int:
+23        if v < 0:
+24            raise ValueError("bytes must be positive")
+25        return v
+
+ + +

Data transfer limit configuration.

+
+ + +
+
+ bytes: int + + +
+ + + + +
+
+ +
+
@field_validator('bytes')
+ + def + validate_bytes(cls, v: int) -> int: + + + +
+ +
21    @field_validator("bytes")
+22    def validate_bytes(cls, v: int) -> int:
+23        if v < 0:
+24            raise ValueError("bytes must be positive")
+25        return v
+
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + ErrorResponse(pydantic.main.BaseModel): + + + +
+ +
124class ErrorResponse(BaseModel):
+125    """
+126    Error response structure
+127    Per OpenAPI: 404 and 400 responses
+128    """
+129
+130    code: str
+131    message: str
+
+ + +

Error response structure +Per OpenAPI: 404 and 400 responses

+
+ + +
+
+ code: str + + +
+ + + + +
+
+
+ message: str + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + ExperimentalMetrics(pydantic.main.BaseModel): + + + +
+ +
79class ExperimentalMetrics(BaseModel):
+80    """
+81    Experimental metrics data structure
+82    Per OpenAPI: /experimental/server/metrics endpoint
+83    """
+84
+85    server: list[ServerMetric]
+86    access_keys: list[AccessKeyMetric] = Field(alias="accessKeys")
+
+ + +

Experimental metrics data structure +Per OpenAPI: /experimental/server/metrics endpoint

+
+ + +
+
+ server: list[pyoutlineapi.models.ServerMetric] + + +
+ + + + +
+
+
+ access_keys: list[pyoutlineapi.models.AccessKeyMetric] + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + MetricsPeriod(builtins.str, enum.Enum): + + + +
+ +
 8class MetricsPeriod(str, Enum):
+ 9    """Time periods for metrics collection."""
+10
+11    DAILY = "daily"
+12    WEEKLY = "weekly"
+13    MONTHLY = "monthly"
+
+ + +

Time periods for metrics collection.

+
+ + +
+
+ DAILY = +<MetricsPeriod.DAILY: 'daily'> + + +
+ + + + +
+
+
+ WEEKLY = +<MetricsPeriod.WEEKLY: 'weekly'> + + +
+ + + + +
+
+
+ MONTHLY = +<MetricsPeriod.MONTHLY: 'monthly'> + + +
+ + + + +
+
+
+ +
+ + class + MetricsStatusResponse(pydantic.main.BaseModel): + + + +
+ +
118class MetricsStatusResponse(BaseModel):
+119    """Response for /metrics/enabled endpoint"""
+120
+121    metrics_enabled: bool = Field(alias="metricsEnabled")
+
+ + +

Response for /metrics/enabled endpoint

+
+ + +
+
+ metrics_enabled: bool + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + Server(pydantic.main.BaseModel): + + + +
+ +
 89class Server(BaseModel):
+ 90    """
+ 91    Server information.
+ 92    Per OpenAPI: /server endpoint schema
+ 93    """
+ 94
+ 95    name: str
+ 96    server_id: str = Field(alias="serverId")
+ 97    metrics_enabled: bool = Field(alias="metricsEnabled")
+ 98    created_timestamp_ms: int = Field(alias="createdTimestampMs")
+ 99    version: str
+100    port_for_new_access_keys: int = Field(alias="portForNewAccessKeys", gt=0, lt=65536)
+101    hostname_for_access_keys: Optional[str] = Field(None, alias="hostnameForAccessKeys")
+102    access_key_data_limit: Optional[DataLimit] = Field(None, alias="accessKeyDataLimit")
+
+ + +

Server information. +Per OpenAPI: /server endpoint schema

+
+ + +
+
+ name: str + + +
+ + + + +
+
+
+ server_id: str + + +
+ + + + +
+
+
+ metrics_enabled: bool + + +
+ + + + +
+
+
+ created_timestamp_ms: int + + +
+ + + + +
+
+
+ version: str + + +
+ + + + +
+
+
+ port_for_new_access_keys: int + + +
+ + + + +
+
+
+ hostname_for_access_keys: Optional[str] + + +
+ + + + +
+
+
+ access_key_data_limit: Optional[DataLimit] + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + ServerMetrics(pydantic.main.BaseModel): + + + +
+ +
46class ServerMetrics(BaseModel):
+47    """
+48    Server metrics data for data transferred per access key
+49    Per OpenAPI: /metrics/transfer endpoint
+50    """
+51
+52    bytes_transferred_by_user_id: dict[str, int] = Field(
+53        alias="bytesTransferredByUserId"
+54    )
+
+ + +

Server metrics data for data transferred per access key +Per OpenAPI: /metrics/transfer endpoint

+
+ + +
+
+ bytes_transferred_by_user_id: dict[str, int] + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ + \ No newline at end of file diff --git a/docs/search.js b/docs/search.js new file mode 100644 index 0000000..6fbb620 --- /dev/null +++ b/docs/search.js @@ -0,0 +1,46 @@ +window.pdocSearch = (function(){ +/** elasticlunr - http://weixsong.github.io * Copyright (C) 2017 Oliver Nightingale * Copyright (C) 2017 Wei Song * MIT Licensed */!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();o

\n"}, {"fullname": "pyoutlineapi.AsyncOutlineClient", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient", "kind": "class", "doc": "

Asynchronous client for the Outline VPN Server API.

\n\n
Arguments:
\n\n
    \n
  • api_url: Base URL for the Outline server API
  • \n
  • cert_sha256: SHA-256 fingerprint of the server's TLS certificate
  • \n
  • json_format: Return raw JSON instead of Pydantic models
  • \n
  • timeout: Request timeout in seconds
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         server_info = await client.get_server_info()\n
\n
\n
\n"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.__init__", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.__init__", "kind": "function", "doc": "

\n", "signature": "(\tapi_url: str,\tcert_sha256: str,\t*,\tjson_format: bool = True,\ttimeout: float = 30.0)"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_server_info", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_server_info", "kind": "function", "doc": "

Get server information.

\n\n
Returns:
\n\n
\n

Server information including name, ID, and configuration.

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         server = await client.get_server_info()\n...         print(f"Server {server.name} running version {server.version}")\n
\n
\n
\n", "signature": "(self) -> Union[dict[str, Any], pyoutlineapi.models.Server]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.rename_server", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.rename_server", "kind": "function", "doc": "

Rename the server.

\n\n
Arguments:
\n\n
    \n
  • name: New server name
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...     success = await client.rename_server("My VPN Server")\n...     if success:\n...         print("Server renamed successfully")\n
\n
\n
\n", "signature": "(self, name: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_hostname", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_hostname", "kind": "function", "doc": "

Set server hostname for access keys.

\n\n
Arguments:
\n\n
    \n
  • hostname: New hostname or IP address
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If hostname is invalid
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         await client.set_hostname("vpn.example.com")\n...         # Or use IP address\n...         await client.set_hostname("203.0.113.1")\n
\n
\n
\n", "signature": "(self, hostname: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_default_port", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_default_port", "kind": "function", "doc": "

Set default port for new access keys.

\n\n
Arguments:
\n\n
    \n
  • port: Port number (1025-65535)
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If port is invalid or in use
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         await client.set_default_port(8388)\n
\n
\n
\n", "signature": "(self, port: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_metrics_status", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_metrics_status", "kind": "function", "doc": "

Get whether metrics collection is enabled.

\n\n
Returns:
\n\n
\n

Current metrics collection status

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         if await client.get_metrics_status():\n...             print("Metrics collection is enabled")\n
\n
\n
\n", "signature": "(self) -> dict[str, typing.Any] | pydantic.main.BaseModel:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_metrics_status", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_metrics_status", "kind": "function", "doc": "

Enable or disable metrics collection.

\n\n
Arguments:
\n\n
    \n
  • enabled: Whether to enable metrics
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Enable metrics\n...         await client.set_metrics_status(True)\n...         # Check new status\n...         is_enabled = await client.get_metrics_status()\n
\n
\n
\n", "signature": "(self, enabled: bool) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_transfer_metrics", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_transfer_metrics", "kind": "function", "doc": "

Get transfer metrics for specified period.

\n\n
Arguments:
\n\n
    \n
  • period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
  • \n
\n\n
Returns:
\n\n
\n

Transfer metrics data for each access key

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Get monthly metrics\n...         metrics = await client.get_transfer_metrics()\n...         # Or get daily metrics\n...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)\n...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():\n...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")\n
\n
\n
\n", "signature": "(\tself,\tperiod: pyoutlineapi.models.MetricsPeriod = <MetricsPeriod.MONTHLY: 'monthly'>) -> Union[dict[str, Any], pyoutlineapi.models.ServerMetrics]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.create_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.create_access_key", "kind": "function", "doc": "

Create a new access key.

\n\n
Arguments:
\n\n
    \n
  • name: Optional key name
  • \n
  • password: Optional password
  • \n
  • port: Optional port number (1-65535)
  • \n
  • method: Optional encryption method
  • \n
  • limit: Optional data transfer limit
  • \n
\n\n
Returns:
\n\n
\n

New access key details

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Create basic key\n...         key = await client.create_access_key(name="User 1")\n...\n...         # Create key with data limit\n...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB\n...         key = await client.create_access_key(\n...             name="Limited User",\n...             port=8388,\n...             limit=_limit\n...         )\n...         print(f"Created key: {key.access_url}")\n
\n
\n
\n", "signature": "(\tself,\t*,\tname: Optional[str] = None,\tpassword: Optional[str] = None,\tport: Optional[int] = None,\tmethod: Optional[str] = None,\tlimit: Optional[pyoutlineapi.models.DataLimit] = None) -> Union[dict[str, Any], pyoutlineapi.models.AccessKey]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_access_keys", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_access_keys", "kind": "function", "doc": "

Get all access keys.

\n\n
Returns:
\n\n
\n

List of all access keys

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         keys = await client.get_access_keys()\n...         for key in keys.access_keys:\n...             print(f"Key {key.id}: {key.name or 'unnamed'}")\n...             if key.data_limit:\n...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")\n
\n
\n
\n", "signature": "(self) -> Union[dict[str, Any], pyoutlineapi.models.AccessKeyList]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_access_key", "kind": "function", "doc": "

Get specific access key.

\n\n
Arguments:
\n\n
    \n
  • key_id: Access key ID
  • \n
\n\n
Returns:
\n\n
\n

Access key details

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If key doesn't exist
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         key = await client.get_access_key(1)\n...         print(f"Port: {key.port}")\n...         print(f"URL: {key.access_url}")\n
\n
\n
\n", "signature": "(\tself,\tkey_id: int) -> Union[dict[str, Any], pyoutlineapi.models.AccessKey]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.rename_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.rename_access_key", "kind": "function", "doc": "

Rename access key.

\n\n
Arguments:
\n\n
    \n
  • key_id: Access key ID
  • \n
  • name: New name
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If key doesn't exist
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Rename key\n...         await client.rename_access_key(1, "Alice")\n...\n...         # Verify new name\n...         key = await client.get_access_key(1)\n...         assert key.name == "Alice"\n
\n
\n
\n", "signature": "(self, key_id: int, name: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.delete_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.delete_access_key", "kind": "function", "doc": "

Delete access key.

\n\n
Arguments:
\n\n
    \n
  • key_id: Access key ID
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If key doesn't exist
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         if await client.delete_access_key(1):\n...             print("Key deleted")\n
\n
\n
\n", "signature": "(self, key_id: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_access_key_data_limit", "kind": "function", "doc": "

Set data transfer limit for access key.

\n\n
Arguments:
\n\n
    \n
  • key_id: Access key ID
  • \n
  • bytes_limit: Limit in bytes (must be positive)
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If key doesn't exist or limit is invalid
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Set 5 GB limit\n...         limit = 5 * 1024**3  # 5 GB in bytes\n...         await client.set_access_key_data_limit(1, limit)\n...\n...         # Verify limit\n...         key = await client.get_access_key(1)\n...         assert key.data_limit and key.data_limit.bytes == limit\n
\n
\n
\n", "signature": "(self, key_id: int, bytes_limit: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.remove_access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.remove_access_key_data_limit", "kind": "function", "doc": "

Remove data transfer limit from access key.

\n\n
Arguments:
\n\n
    \n
  • key_id: Access key ID
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If key doesn't exist
  • \n
\n", "signature": "(self, key_id: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.OutlineError", "modulename": "pyoutlineapi", "qualname": "OutlineError", "kind": "class", "doc": "

Base exception for Outline client errors.

\n", "bases": "builtins.Exception"}, {"fullname": "pyoutlineapi.APIError", "modulename": "pyoutlineapi", "qualname": "APIError", "kind": "class", "doc": "

Raised when API requests fail.

\n", "bases": "pyoutlineapi.client.OutlineError"}, {"fullname": "pyoutlineapi.AccessKey", "modulename": "pyoutlineapi", "qualname": "AccessKey", "kind": "class", "doc": "

Access key details.

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKey.id", "modulename": "pyoutlineapi", "qualname": "AccessKey.id", "kind": "variable", "doc": "

\n", "annotation": ": int"}, {"fullname": "pyoutlineapi.AccessKey.name", "modulename": "pyoutlineapi", "qualname": "AccessKey.name", "kind": "variable", "doc": "

\n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKey.password", "modulename": "pyoutlineapi", "qualname": "AccessKey.password", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.port", "modulename": "pyoutlineapi", "qualname": "AccessKey.port", "kind": "variable", "doc": "

\n", "annotation": ": int"}, {"fullname": "pyoutlineapi.AccessKey.method", "modulename": "pyoutlineapi", "qualname": "AccessKey.method", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.access_url", "modulename": "pyoutlineapi", "qualname": "AccessKey.access_url", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.data_limit", "modulename": "pyoutlineapi", "qualname": "AccessKey.data_limit", "kind": "variable", "doc": "

\n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.AccessKey.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKey.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest", "kind": "class", "doc": "

Request parameters for creating an access key.\nPer OpenAPI: /access-keys POST request body

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.name", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.name", "kind": "variable", "doc": "

\n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.method", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.method", "kind": "variable", "doc": "

\n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.password", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.password", "kind": "variable", "doc": "

\n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.port", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.port", "kind": "variable", "doc": "

\n", "annotation": ": Optional[int]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.limit", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.limit", "kind": "variable", "doc": "

\n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyList", "modulename": "pyoutlineapi", "qualname": "AccessKeyList", "kind": "class", "doc": "

List of access keys.

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKeyList.access_keys", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.access_keys", "kind": "variable", "doc": "

\n", "annotation": ": list[pyoutlineapi.models.AccessKey]"}, {"fullname": "pyoutlineapi.AccessKeyList.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.DataLimit", "modulename": "pyoutlineapi", "qualname": "DataLimit", "kind": "class", "doc": "

Data transfer limit configuration.

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.DataLimit.bytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.bytes", "kind": "variable", "doc": "

\n", "annotation": ": int"}, {"fullname": "pyoutlineapi.DataLimit.validate_bytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.validate_bytes", "kind": "function", "doc": "

\n", "signature": "(cls, v: int) -> int:", "funcdef": "def"}, {"fullname": "pyoutlineapi.DataLimit.model_config", "modulename": "pyoutlineapi", "qualname": "DataLimit.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ErrorResponse", "modulename": "pyoutlineapi", "qualname": "ErrorResponse", "kind": "class", "doc": "

Error response structure\nPer OpenAPI: 404 and 400 responses

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ErrorResponse.code", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.code", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.ErrorResponse.message", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.message", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.ErrorResponse.model_config", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ExperimentalMetrics", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics", "kind": "class", "doc": "

Experimental metrics data structure\nPer OpenAPI: /experimental/server/metrics endpoint

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.server", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.server", "kind": "variable", "doc": "

\n", "annotation": ": list[pyoutlineapi.models.ServerMetric]"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.access_keys", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.access_keys", "kind": "variable", "doc": "

\n", "annotation": ": list[pyoutlineapi.models.AccessKeyMetric]"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.model_config", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.MetricsPeriod", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod", "kind": "class", "doc": "

Time periods for metrics collection.

\n", "bases": "builtins.str, enum.Enum"}, {"fullname": "pyoutlineapi.MetricsPeriod.DAILY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.DAILY", "kind": "variable", "doc": "

\n", "default_value": "<MetricsPeriod.DAILY: 'daily'>"}, {"fullname": "pyoutlineapi.MetricsPeriod.WEEKLY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.WEEKLY", "kind": "variable", "doc": "

\n", "default_value": "<MetricsPeriod.WEEKLY: 'weekly'>"}, {"fullname": "pyoutlineapi.MetricsPeriod.MONTHLY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.MONTHLY", "kind": "variable", "doc": "

\n", "default_value": "<MetricsPeriod.MONTHLY: 'monthly'>"}, {"fullname": "pyoutlineapi.MetricsStatusResponse", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse", "kind": "class", "doc": "

Response for /metrics/enabled endpoint

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.MetricsStatusResponse.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse.metrics_enabled", "kind": "variable", "doc": "

\n", "annotation": ": bool"}, {"fullname": "pyoutlineapi.MetricsStatusResponse.model_config", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.Server", "modulename": "pyoutlineapi", "qualname": "Server", "kind": "class", "doc": "

Server information.\nPer OpenAPI: /server endpoint schema

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.Server.name", "modulename": "pyoutlineapi", "qualname": "Server.name", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.server_id", "modulename": "pyoutlineapi", "qualname": "Server.server_id", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "Server.metrics_enabled", "kind": "variable", "doc": "

\n", "annotation": ": bool"}, {"fullname": "pyoutlineapi.Server.created_timestamp_ms", "modulename": "pyoutlineapi", "qualname": "Server.created_timestamp_ms", "kind": "variable", "doc": "

\n", "annotation": ": int"}, {"fullname": "pyoutlineapi.Server.version", "modulename": "pyoutlineapi", "qualname": "Server.version", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.port_for_new_access_keys", "modulename": "pyoutlineapi", "qualname": "Server.port_for_new_access_keys", "kind": "variable", "doc": "

\n", "annotation": ": int"}, {"fullname": "pyoutlineapi.Server.hostname_for_access_keys", "modulename": "pyoutlineapi", "qualname": "Server.hostname_for_access_keys", "kind": "variable", "doc": "

\n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.Server.access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "Server.access_key_data_limit", "kind": "variable", "doc": "

\n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.Server.model_config", "modulename": "pyoutlineapi", "qualname": "Server.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ServerMetrics", "modulename": "pyoutlineapi", "qualname": "ServerMetrics", "kind": "class", "doc": "

Server metrics data for data transferred per access key\nPer OpenAPI: /metrics/transfer endpoint

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.bytes_transferred_by_user_id", "kind": "variable", "doc": "

\n", "annotation": ": dict[str, int]"}, {"fullname": "pyoutlineapi.ServerMetrics.model_config", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}]; + + // mirrored in build-search-index.js (part 1) + // Also split on html tags. this is a cheap heuristic, but good enough. + elasticlunr.tokenizer.setSeperator(/[\s\-.;&_'"=,()]+|<[^>]*>/); + + let searchIndex; + if (docs._isPrebuiltIndex) { + console.info("using precompiled search index"); + searchIndex = elasticlunr.Index.load(docs); + } else { + console.time("building search index"); + // mirrored in build-search-index.js (part 2) + searchIndex = elasticlunr(function () { + this.pipeline.remove(elasticlunr.stemmer); + this.pipeline.remove(elasticlunr.stopWordFilter); + this.addField("qualname"); + this.addField("fullname"); + this.addField("annotation"); + this.addField("default_value"); + this.addField("signature"); + this.addField("bases"); + this.addField("doc"); + this.setRef("fullname"); + }); + for (let doc of docs) { + searchIndex.addDoc(doc); + } + console.timeEnd("building search index"); + } + + return (term) => searchIndex.search(term, { + fields: { + qualname: {boost: 4}, + fullname: {boost: 2}, + annotation: {boost: 2}, + default_value: {boost: 2}, + signature: {boost: 2}, + bases: {boost: 2}, + doc: {boost: 1}, + }, + expand: true + }); +})(); \ No newline at end of file