fport


Namefport JSON
Version 1.0.3 PyPI version JSON
download
home_pageNone
SummaryLoosely-coupled, one-way function wiring for white-box testing and simple add-ons.
upload_time2025-08-19 16:37:45
maintainerNone
docs_urlNone
authorminoru_jp
requires_python>=3.10
licenseMIT License Copyright (c) 2025 minoru-jp Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
keywords whitebox-testing testing observer add-on loose-coupling
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # fport

## Version

[![PyPI version](https://img.shields.io/pypi/v/fport.svg)](https://pypi.org/project/fport/)

## Overview

A generic unidirectional function coupling module based on loose coupling.

This module provides an interface for the implementation side to send information.

## Main Purpose and Use Cases

* Submitting information from the implementation side for white-box testing
* Creating entry points for simple add-ons

## Supported Environment

* Python 3.10 or later
* No external dependencies

## License

This module is provided under the MIT License.
See the [LICENSE](./LICENSE) file for details.

## Installation

PyPI
```bash
pip install fport
```

GitHub
```bash
pip install git+https://github.com/minoru_jp/fport.git
```

## Features

* Provides a communication channel to the implementation side with minimal setup.
* Designed so that the sending interface has no side effects on the implementation side (only computation cost on the receiving side).
* The sending interface does not propagate errors from the receiver or framework to the implementation side.
* The sending interface always returns `None`.
* The scope of information transfer can be flexibly defined by where and how the interface is defined and shared.
* You can configure the sending interface to reject connections.
* Even if the connection is rejected, the implementation side always gets a valid interface.

## Warning

The communication mechanism adopted by this module is loosely coupled and does not explicitly specify the destination from the implementation side.
Information transmitted from the implementation must be carefully considered. Careless transmission may lead to leaks of authentication data, personal information, or other critical data. The same applies to information that can be reconstructed into such sensitive data.

## About Parallel Processing

The sending interface is **thread-unsafe**.
This design avoids unintended serialization on the implementation side.
Maintaining overall consistency, including the use of interfaces in parallel processing, is the responsibility of the implementation side.

## Simple Usage Example

```python
from fport import create_session_policy

policy = create_session_policy()
port = policy.create_port()

def add(a, b):
    port.send("add", a, b)
    return a + b

def listener(tag, *args, **kwargs):
    print("Received:", tag, args, kwargs)

with policy.session(listener, port) as state:
    result = add(2, 3)
    print("Result:", result)

# Output:
# Received: add (2, 3) {}
# Result: 5
```

---

## Main API Reference

### `create_session_policy(*, block_port: bool = False, message_validator: SendFunction | None = None) -> SessionPolicy`

Factory function to generate a `SessionPolicy`.

* **Parameters**

  * `block_port: bool`
    If `True`, all `Port`s created by this policy reject connections.
  * `message_validator: SendFunction | None`
    Optional validation function for sending. Called before `Port.send()`.
    If an exception is raised, the send is rejected.
    The exception does not propagate to the sender; instead, it is treated as a session termination.

* **Returns**
  `SessionPolicy`

---

### `class SessionPolicy`

Interface for managing `Port` creation and session establishment.

* **Methods**

  * `create_port() -> Port`
    Creates a connectable `Port`.

  * `create_noop_port() -> Port`
    Creates a no-op `Port` that rejects connections.

  * `session(listener: ListenFunction, target: Port) -> ContextManager[SessionState]`
    Returns a context manager to start a session by connecting `listener` to the specified `Port`.

    * **Parameters**

      * `listener: ListenFunction`
        A callback function that receives messages sent via `Port.send()`.
        Takes arguments `(tag: str, *args, **kwargs)`.
      * `target: Port`
        The target `Port` instance.

    * **Returns**
      `ContextManager[SessionState]`
      Used in a `with` block. Provides `SessionState` for monitoring with `ok` and `error`.

    * **Exceptions**

      * `TypeError`: If `target` is not a `Port` instance
      * `OccupiedError`: If the specified `Port` is already used by another session
      * `DeniedError`: If the `Port` or `SessionPolicy` is set to reject connections
      * `RuntimeError`: Unexpected internal inconsistencies

---

### `class Port`

Interface for the implementation (sending side) to transmit information.

* **Methods**

  * `send(tag: str, *args, **kwargs) -> None`
    Sends arbitrary information to registered listeners.

    * Does nothing if no listener is registered
    * Exceptions are not propagated to the sender (fail-silent)
    * **Thread-unsafe**: designed to avoid unintended serialization

---

### `class SessionState`

Read-only interface for monitoring session status.

* **Properties**

  * `ok: bool`
    Whether the session is still active
  * `error: Exception | None`
    The first error that caused the session to end, or `None`

---

### Exceptions

* `class DeniedError(Exception)`
  Raised when a policy or `Port` rejects a connection.

* `class OccupiedError(Exception)`
  Raised when a `Port` is already occupied by another session.

---

### Protocols (Types)

* `class SendFunction(Protocol)`

  ```python
  def __call__(tag: str, *args, **kwargs) -> None
  ```

  Callable object used by the sender to send messages.

* `class ListenFunction(Protocol)`

  ```python
  def __call__(tag: str, *args, **kwargs) -> None
  ```

  Callable object used by the receiver to process messages.

---

## Observer

This library includes an `observer` implementation as a listener.

### Example usage with fport

```python
from fport import create_session_policy
from fport.observer import ProcessObserver

def create_weather_sensor(port):
    """Weather sensor
    Specification:
        temp < 0        -> "Freezing" + send("freezing")
        0 <= temp <= 30 -> "Normal"   + send("normal")
        temp > 30       -> "Hot"      + send("hot")
    """
    def check_weather(temp: int) -> str:
        # If there is a bug here, it will be detected by the test
        if temp <= 0:   # ← Common place to inject a bug
            port.send("freezing", temp)
            return "Freezing"
        elif temp <= 30:
            port.send("normal", temp)
            return "Normal"
        else:
            port.send("hot", temp)
            return "Hot"
    return check_weather

policy = create_session_policy()
port = policy.create_port()

# Define expected conditions according to the specification
conditions = {
    "freezing": lambda t: t < 0,
    "normal":   lambda t: 0 <= t <= 30,
    "hot":      lambda t: t > 30,
}
observer = ProcessObserver(conditions)
check_weather = create_weather_sensor(port)

with policy.session(observer.listen, port) as state:
    # Test coverage for all three branches
    for i in (-5, 0, 31):
        check_weather(i)
        if not state.ok:
            raise AssertionError(f"observation failed on '{i}'")

    # Verify that the Observer did not detect any specification violations
    if observer.violation:
        details = []
        for tag, obs in observer.get_violated().items():
            details.append(
                f"[{tag}] reason={obs.fail_reason}, "
                f"count={obs.count}, first_violation_at={obs.first_violation_at}"
            )
        raise AssertionError("Observer detected violations:\n" + "\n".join(details))

print("All checks passed!")
```

---

## Observer API Reference

### Class `ProcessObserver`

Monitors process state, handling condition violations and exceptions.

#### Constructor

```python
ProcessObserver(conditions: dict[str, Callable[..., bool]])
```

Initializes with the given set of conditions to monitor.

#### Methods

* `reset_observations() -> None`
  Reset all observation results.

* `listen(tag: str, *args, **kwargs) -> None`
  Evaluate the condition for the given tag.
  Calls handlers on violation or exception.

* `get_all() -> dict[str, Observation]`
  Returns all observation results.

* `get_violated() -> dict[str, Observation]`
  Returns observations where violations occurred.

* `get_compliant() -> dict[str, Observation]`
  Returns observations with no violations.

* `get_unevaluated() -> dict[str, Observation]`
  Returns unevaluated observations.

* `set_violation_handler(tag: str, fn: Callable[[Observation], None]) -> None`
  Sets a violation handler for the specified tag.

* `set_exception_handler(fn: Callable[[str, ExceptionKind, Observation | None, Exception], None]) -> None`
  Sets an exception handler.

* `get_stat(tag: str) -> ConditionStat`
  Returns statistical information for the specified tag.

#### Properties

* `violation: bool`
  Whether any violation exists.

* `global_violation: bool`
  Whether any global violation exists.

* `local_violation: bool`
  Whether any local violation exists.

* `global_fail_reason: str`
  Returns the reason for the global violation.

* `global_exception: Exception | None`
  Returns the global exception, if any.

---

### Class `Observation`

Holds detailed observation results per condition.

#### Fields

* `count: int` – Number of evaluations
* `violation: bool` – Whether a violation occurred
* `first_violation_at: int` – Trial number of the first violation
* `exc: Exception | None` – Exception that occurred
* `fail_condition: Callable[..., bool] | None` – Condition function that failed
* `fail_reason: str` – Reason for the violation

---

### Class `ConditionStat`

Simplified statistical representation of condition results.

#### Constructor

```python
ConditionStat(count: int, violation: bool, first_violation_at: int)
```

#### Properties

* `count: int` – Number of evaluations
* `violation: bool` – Whether a violation occurred
* `first_violation_at: int` – Trial number of the first violation

---

### Enum `ExceptionKind`

Indicates where an exception occurred.

#### Constants

* `ON_CONDITION` – Exception during condition evaluation
* `ON_VIOLATION` – Exception during violation handler execution
* `ON_INTERNAL` – Exception during internal processing

---

## Testing

This module uses `pytest` for testing.
Tests are located in the `tests/` directory.
The `legacy/` directory contains disabled tests and should be skipped.


            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "fport",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.10",
    "maintainer_email": null,
    "keywords": "whitebox-testing, testing, observer, add-on, loose-coupling",
    "author": "minoru_jp",
    "author_email": null,
    "download_url": "https://files.pythonhosted.org/packages/2d/83/93c56fb0197ec610cdfeb36a6e98d6f81242e80f6f891a136f66533abe58/fport-1.0.3.tar.gz",
    "platform": null,
    "description": "# fport\r\n\r\n## Version\r\n\r\n[![PyPI version](https://img.shields.io/pypi/v/fport.svg)](https://pypi.org/project/fport/)\r\n\r\n## Overview\r\n\r\nA generic unidirectional function coupling module based on loose coupling.\r\n\r\nThis module provides an interface for the implementation side to send information.\r\n\r\n## Main Purpose and Use Cases\r\n\r\n* Submitting information from the implementation side for white-box testing\r\n* Creating entry points for simple add-ons\r\n\r\n## Supported Environment\r\n\r\n* Python 3.10 or later\r\n* No external dependencies\r\n\r\n## License\r\n\r\nThis module is provided under the MIT License.\r\nSee the [LICENSE](./LICENSE) file for details.\r\n\r\n## Installation\r\n\r\nPyPI\r\n```bash\r\npip install fport\r\n```\r\n\r\nGitHub\r\n```bash\r\npip install git+https://github.com/minoru_jp/fport.git\r\n```\r\n\r\n## Features\r\n\r\n* Provides a communication channel to the implementation side with minimal setup.\r\n* Designed so that the sending interface has no side effects on the implementation side (only computation cost on the receiving side).\r\n* The sending interface does not propagate errors from the receiver or framework to the implementation side.\r\n* The sending interface always returns `None`.\r\n* The scope of information transfer can be flexibly defined by where and how the interface is defined and shared.\r\n* You can configure the sending interface to reject connections.\r\n* Even if the connection is rejected, the implementation side always gets a valid interface.\r\n\r\n## Warning\r\n\r\nThe communication mechanism adopted by this module is loosely coupled and does not explicitly specify the destination from the implementation side.\r\nInformation transmitted from the implementation must be carefully considered. Careless transmission may lead to leaks of authentication data, personal information, or other critical data. The same applies to information that can be reconstructed into such sensitive data.\r\n\r\n## About Parallel Processing\r\n\r\nThe sending interface is **thread-unsafe**.\r\nThis design avoids unintended serialization on the implementation side.\r\nMaintaining overall consistency, including the use of interfaces in parallel processing, is the responsibility of the implementation side.\r\n\r\n## Simple Usage Example\r\n\r\n```python\r\nfrom fport import create_session_policy\r\n\r\npolicy = create_session_policy()\r\nport = policy.create_port()\r\n\r\ndef add(a, b):\r\n    port.send(\"add\", a, b)\r\n    return a + b\r\n\r\ndef listener(tag, *args, **kwargs):\r\n    print(\"Received:\", tag, args, kwargs)\r\n\r\nwith policy.session(listener, port) as state:\r\n    result = add(2, 3)\r\n    print(\"Result:\", result)\r\n\r\n# Output:\r\n# Received: add (2, 3) {}\r\n# Result: 5\r\n```\r\n\r\n---\r\n\r\n## Main API Reference\r\n\r\n### `create_session_policy(*, block_port: bool = False, message_validator: SendFunction | None = None) -> SessionPolicy`\r\n\r\nFactory function to generate a `SessionPolicy`.\r\n\r\n* **Parameters**\r\n\r\n  * `block_port: bool`\r\n    If `True`, all `Port`s created by this policy reject connections.\r\n  * `message_validator: SendFunction | None`\r\n    Optional validation function for sending. Called before `Port.send()`.\r\n    If an exception is raised, the send is rejected.\r\n    The exception does not propagate to the sender; instead, it is treated as a session termination.\r\n\r\n* **Returns**\r\n  `SessionPolicy`\r\n\r\n---\r\n\r\n### `class SessionPolicy`\r\n\r\nInterface for managing `Port` creation and session establishment.\r\n\r\n* **Methods**\r\n\r\n  * `create_port() -> Port`\r\n    Creates a connectable `Port`.\r\n\r\n  * `create_noop_port() -> Port`\r\n    Creates a no-op `Port` that rejects connections.\r\n\r\n  * `session(listener: ListenFunction, target: Port) -> ContextManager[SessionState]`\r\n    Returns a context manager to start a session by connecting `listener` to the specified `Port`.\r\n\r\n    * **Parameters**\r\n\r\n      * `listener: ListenFunction`\r\n        A callback function that receives messages sent via `Port.send()`.\r\n        Takes arguments `(tag: str, *args, **kwargs)`.\r\n      * `target: Port`\r\n        The target `Port` instance.\r\n\r\n    * **Returns**\r\n      `ContextManager[SessionState]`\r\n      Used in a `with` block. Provides `SessionState` for monitoring with `ok` and `error`.\r\n\r\n    * **Exceptions**\r\n\r\n      * `TypeError`: If `target` is not a `Port` instance\r\n      * `OccupiedError`: If the specified `Port` is already used by another session\r\n      * `DeniedError`: If the `Port` or `SessionPolicy` is set to reject connections\r\n      * `RuntimeError`: Unexpected internal inconsistencies\r\n\r\n---\r\n\r\n### `class Port`\r\n\r\nInterface for the implementation (sending side) to transmit information.\r\n\r\n* **Methods**\r\n\r\n  * `send(tag: str, *args, **kwargs) -> None`\r\n    Sends arbitrary information to registered listeners.\r\n\r\n    * Does nothing if no listener is registered\r\n    * Exceptions are not propagated to the sender (fail-silent)\r\n    * **Thread-unsafe**: designed to avoid unintended serialization\r\n\r\n---\r\n\r\n### `class SessionState`\r\n\r\nRead-only interface for monitoring session status.\r\n\r\n* **Properties**\r\n\r\n  * `ok: bool`\r\n    Whether the session is still active\r\n  * `error: Exception | None`\r\n    The first error that caused the session to end, or `None`\r\n\r\n---\r\n\r\n### Exceptions\r\n\r\n* `class DeniedError(Exception)`\r\n  Raised when a policy or `Port` rejects a connection.\r\n\r\n* `class OccupiedError(Exception)`\r\n  Raised when a `Port` is already occupied by another session.\r\n\r\n---\r\n\r\n### Protocols (Types)\r\n\r\n* `class SendFunction(Protocol)`\r\n\r\n  ```python\r\n  def __call__(tag: str, *args, **kwargs) -> None\r\n  ```\r\n\r\n  Callable object used by the sender to send messages.\r\n\r\n* `class ListenFunction(Protocol)`\r\n\r\n  ```python\r\n  def __call__(tag: str, *args, **kwargs) -> None\r\n  ```\r\n\r\n  Callable object used by the receiver to process messages.\r\n\r\n---\r\n\r\n## Observer\r\n\r\nThis library includes an `observer` implementation as a listener.\r\n\r\n### Example usage with fport\r\n\r\n```python\r\nfrom fport import create_session_policy\r\nfrom fport.observer import ProcessObserver\r\n\r\ndef create_weather_sensor(port):\r\n    \"\"\"Weather sensor\r\n    Specification:\r\n        temp < 0        -> \"Freezing\" + send(\"freezing\")\r\n        0 <= temp <= 30 -> \"Normal\"   + send(\"normal\")\r\n        temp > 30       -> \"Hot\"      + send(\"hot\")\r\n    \"\"\"\r\n    def check_weather(temp: int) -> str:\r\n        # If there is a bug here, it will be detected by the test\r\n        if temp <= 0:   # \u2190 Common place to inject a bug\r\n            port.send(\"freezing\", temp)\r\n            return \"Freezing\"\r\n        elif temp <= 30:\r\n            port.send(\"normal\", temp)\r\n            return \"Normal\"\r\n        else:\r\n            port.send(\"hot\", temp)\r\n            return \"Hot\"\r\n    return check_weather\r\n\r\npolicy = create_session_policy()\r\nport = policy.create_port()\r\n\r\n# Define expected conditions according to the specification\r\nconditions = {\r\n    \"freezing\": lambda t: t < 0,\r\n    \"normal\":   lambda t: 0 <= t <= 30,\r\n    \"hot\":      lambda t: t > 30,\r\n}\r\nobserver = ProcessObserver(conditions)\r\ncheck_weather = create_weather_sensor(port)\r\n\r\nwith policy.session(observer.listen, port) as state:\r\n    # Test coverage for all three branches\r\n    for i in (-5, 0, 31):\r\n        check_weather(i)\r\n        if not state.ok:\r\n            raise AssertionError(f\"observation failed on '{i}'\")\r\n\r\n    # Verify that the Observer did not detect any specification violations\r\n    if observer.violation:\r\n        details = []\r\n        for tag, obs in observer.get_violated().items():\r\n            details.append(\r\n                f\"[{tag}] reason={obs.fail_reason}, \"\r\n                f\"count={obs.count}, first_violation_at={obs.first_violation_at}\"\r\n            )\r\n        raise AssertionError(\"Observer detected violations:\\n\" + \"\\n\".join(details))\r\n\r\nprint(\"All checks passed!\")\r\n```\r\n\r\n---\r\n\r\n## Observer API Reference\r\n\r\n### Class `ProcessObserver`\r\n\r\nMonitors process state, handling condition violations and exceptions.\r\n\r\n#### Constructor\r\n\r\n```python\r\nProcessObserver(conditions: dict[str, Callable[..., bool]])\r\n```\r\n\r\nInitializes with the given set of conditions to monitor.\r\n\r\n#### Methods\r\n\r\n* `reset_observations() -> None`\r\n  Reset all observation results.\r\n\r\n* `listen(tag: str, *args, **kwargs) -> None`\r\n  Evaluate the condition for the given tag.\r\n  Calls handlers on violation or exception.\r\n\r\n* `get_all() -> dict[str, Observation]`\r\n  Returns all observation results.\r\n\r\n* `get_violated() -> dict[str, Observation]`\r\n  Returns observations where violations occurred.\r\n\r\n* `get_compliant() -> dict[str, Observation]`\r\n  Returns observations with no violations.\r\n\r\n* `get_unevaluated() -> dict[str, Observation]`\r\n  Returns unevaluated observations.\r\n\r\n* `set_violation_handler(tag: str, fn: Callable[[Observation], None]) -> None`\r\n  Sets a violation handler for the specified tag.\r\n\r\n* `set_exception_handler(fn: Callable[[str, ExceptionKind, Observation | None, Exception], None]) -> None`\r\n  Sets an exception handler.\r\n\r\n* `get_stat(tag: str) -> ConditionStat`\r\n  Returns statistical information for the specified tag.\r\n\r\n#### Properties\r\n\r\n* `violation: bool`\r\n  Whether any violation exists.\r\n\r\n* `global_violation: bool`\r\n  Whether any global violation exists.\r\n\r\n* `local_violation: bool`\r\n  Whether any local violation exists.\r\n\r\n* `global_fail_reason: str`\r\n  Returns the reason for the global violation.\r\n\r\n* `global_exception: Exception | None`\r\n  Returns the global exception, if any.\r\n\r\n---\r\n\r\n### Class `Observation`\r\n\r\nHolds detailed observation results per condition.\r\n\r\n#### Fields\r\n\r\n* `count: int` \u2013 Number of evaluations\r\n* `violation: bool` \u2013 Whether a violation occurred\r\n* `first_violation_at: int` \u2013 Trial number of the first violation\r\n* `exc: Exception | None` \u2013 Exception that occurred\r\n* `fail_condition: Callable[..., bool] | None` \u2013 Condition function that failed\r\n* `fail_reason: str` \u2013 Reason for the violation\r\n\r\n---\r\n\r\n### Class `ConditionStat`\r\n\r\nSimplified statistical representation of condition results.\r\n\r\n#### Constructor\r\n\r\n```python\r\nConditionStat(count: int, violation: bool, first_violation_at: int)\r\n```\r\n\r\n#### Properties\r\n\r\n* `count: int` \u2013 Number of evaluations\r\n* `violation: bool` \u2013 Whether a violation occurred\r\n* `first_violation_at: int` \u2013 Trial number of the first violation\r\n\r\n---\r\n\r\n### Enum `ExceptionKind`\r\n\r\nIndicates where an exception occurred.\r\n\r\n#### Constants\r\n\r\n* `ON_CONDITION` \u2013 Exception during condition evaluation\r\n* `ON_VIOLATION` \u2013 Exception during violation handler execution\r\n* `ON_INTERNAL` \u2013 Exception during internal processing\r\n\r\n---\r\n\r\n## Testing\r\n\r\nThis module uses `pytest` for testing.\r\nTests are located in the `tests/` directory.\r\nThe `legacy/` directory contains disabled tests and should be skipped.\r\n\r\n",
    "bugtrack_url": null,
    "license": "MIT License\r\n        \r\n        Copyright (c) 2025 minoru-jp\r\n        \r\n        Permission is hereby granted, free of charge, to any person obtaining a copy\r\n        of this software and associated documentation files (the \"Software\"), to deal\r\n        in the Software without restriction, including without limitation the rights\r\n        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r\n        copies of the Software, and to permit persons to whom the Software is\r\n        furnished to do so, subject to the following conditions:\r\n        \r\n        The above copyright notice and this permission notice shall be included in all\r\n        copies or substantial portions of the Software.\r\n        \r\n        THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r\n        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r\n        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r\n        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r\n        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r\n        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\r\n        SOFTWARE.\r\n        ",
    "summary": "Loosely-coupled, one-way function wiring for white-box testing and simple add-ons.",
    "version": "1.0.3",
    "project_urls": {
        "Homepage": "https://github.com/minoru-jp/fport",
        "Issues": "https://github.com/minoru-jp/fport/issues",
        "Source": "https://github.com/minoru-jp/fport"
    },
    "split_keywords": [
        "whitebox-testing",
        " testing",
        " observer",
        " add-on",
        " loose-coupling"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "ed561fa4f269d6b8307dfe4203b2f2905fa7aec5826b98f5268c5c27b837e5f8",
                "md5": "d4ba082a3402db24dfcac51256a1e42e",
                "sha256": "4dd86977450c13fa155cea25e5830684ba998d7d431826c0ab5d710977eb8e0a"
            },
            "downloads": -1,
            "filename": "fport-1.0.3-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "d4ba082a3402db24dfcac51256a1e42e",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.10",
            "size": 16466,
            "upload_time": "2025-08-19T16:37:43",
            "upload_time_iso_8601": "2025-08-19T16:37:43.975031Z",
            "url": "https://files.pythonhosted.org/packages/ed/56/1fa4f269d6b8307dfe4203b2f2905fa7aec5826b98f5268c5c27b837e5f8/fport-1.0.3-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "2d8393c56fb0197ec610cdfeb36a6e98d6f81242e80f6f891a136f66533abe58",
                "md5": "25457951fd4b43bf5140e2254058ee69",
                "sha256": "930792afb312fc67a42c6b6c12e5cd4977f5dec2b034da62c06d6fac53814e08"
            },
            "downloads": -1,
            "filename": "fport-1.0.3.tar.gz",
            "has_sig": false,
            "md5_digest": "25457951fd4b43bf5140e2254058ee69",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.10",
            "size": 17082,
            "upload_time": "2025-08-19T16:37:45",
            "upload_time_iso_8601": "2025-08-19T16:37:45.037933Z",
            "url": "https://files.pythonhosted.org/packages/2d/83/93c56fb0197ec610cdfeb36a6e98d6f81242e80f6f891a136f66533abe58/fport-1.0.3.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-08-19 16:37:45",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "minoru-jp",
    "github_project": "fport",
    "github_not_found": true,
    "lcname": "fport"
}
        
Elapsed time: 1.12339s