ff-logger


Nameff-logger JSON
Version 0.4.1 PyPI version JSON
download
home_pageNone
SummaryFenixflow structured logging package with scoped, instance-based loggers
upload_time2025-10-06 19:41:02
maintainerNone
docs_urlNone
authorNone
requires_python>=3.10
licenseMIT
keywords logging structured-logging json-logging scoped-logger instance-logger fenixflow
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # ff-logger

[![PyPI version](https://badge.fury.io/py/ff-logger.svg)](https://badge.fury.io/py/ff-logger)
[![Python Support](https://img.shields.io/pypi/pyversions/ff-logger.svg)](https://pypi.org/project/ff-logger/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A scoped, instance-based logging package for Fenixflow applications. Unlike traditional Python logging which uses a global configuration, ff-logger provides self-contained logger instances that can be passed around as objects, with support for context binding and multiple output formats.

Created by **Ben Moag** at **[Fenixflow](https://fenixflow.com)**

## Quick Start

### Installation

#### From PyPI (when published)
```bash
pip install ff-logger
```

#### From GitLab (current)
```bash
pip install git+https://gitlab.com/fenixflow/fenix-packages.git#subdirectory=ff-logger
```

### Basic Usage

```python
from ff_logger import ConsoleLogger
import logging

# Create a logger instance with permanent context
logger = ConsoleLogger(
    name="my_app",
    level="INFO",  # Can use strings now! (or logging.INFO)
    context={"service": "api", "environment": "production"}
)

# Log messages with the permanent context
logger.info("Application started")
# Output: [2025-08-20 10:00:00] INFO [my_app] Application started | service="api" environment="production"

# Add runtime context with kwargs
logger.info("Request processed", request_id="req-123", duration=45)
# Output includes both permanent and runtime context
```

### Context Binding

Add permanent context fields to your logger instance:

```python
# Add context that will appear in all subsequent logs
logger.bind(
    request_id="req-456",
    user_id=789,
    ip="192.168.1.1"
)

# All messages now include the bound context
logger.info("Processing payment")
logger.error("Payment failed", error_code="CARD_DECLINED")

# bind() returns self for chaining
logger.bind(session_id="xyz").info("Session started")
```

**Note:** As of v0.3.0, `bind()` modifies the logger instance in place rather than creating a new one. This is cleaner and more intuitive. The method validates that fields are not reserved and values are JSON-serializable.

## Logger Types

### ConsoleLogger
Outputs colored, human-readable logs to console:

```python
from ff_logger import ConsoleLogger

logger = ConsoleLogger(
    name="app",
    level="INFO",  # String or int (logging.INFO)
    colors=True,  # Enable colored output
    show_hostname=False  # Optional hostname in logs
)
```

### JSONLogger
Outputs structured JSON lines, perfect for log aggregation:

```python
from ff_logger import JSONLogger

logger = JSONLogger(
    name="app",
    level="WARNING",  # String or int levels supported
    show_hostname=True,
    include_timestamp=True
)

logger.info("Event occurred", event_type="user_login", user_id=123)
# Output: {"level":"INFO","logger":"app","message":"Event occurred","timestamp":"2025-08-20T10:00:00Z","event_type":"user_login","user_id":123,...}
```

### FileLogger
Writes to files with rotation support:

```python
from ff_logger import FileLogger

logger = FileLogger(
    name="app",
    filename="/var/log/app.log",
    rotation_type="size",  # "size", "time", or "none"
    max_bytes=10*1024*1024,  # 10MB
    backup_count=5
)
```

### NullLogger
Zero-cost logger for testing or when logging is disabled:

```python
from ff_logger import NullLogger

# Preferred: Use directly as a class (no instantiation needed)
NullLogger.info("This does nothing")  # No-op
NullLogger.debug("Debug message")     # No-op

# As a default parameter (perfect for dependency injection)
def process_data(data, logger=NullLogger):
    logger.info("Processing data: %s", data)
    return data * 2

# Call without providing a logger
result = process_data([1, 2, 3])

# Backward compatibility: Can still instantiate if needed
logger = NullLogger()  # All parameters are optional
logger.info("This also does nothing")
```

### DatabaseLogger
Writes logs to a database table (requires ff-storage):

```python
from ff_logger import DatabaseLogger
from ff_storage.db.postgres import PostgresPool

db = PostgresPool(...)
logger = DatabaseLogger(
    name="app",
    db_connection=db,
    table_name="logs",
    schema="public"
)
```

## Key Features

### v0.4.0 Features

#### Temporary Context Manager
Use the `temp_context()` context manager to add temporary fields that are automatically removed:

```python
logger = ConsoleLogger("app")

with logger.temp_context(request_id="123", user_id=456):
    logger.info("Processing request")  # Includes request_id and user_id
    logger.info("Request complete")    # Still includes the fields

# Fields automatically removed after context
logger.info("Next request")  # request_id and user_id no longer present
```

#### Lazy Evaluation for Performance
Pass callables as kwargs to defer expensive computations until needed:

```python
logger = ConsoleLogger("app", level="ERROR")  # Only ERROR and above

# This callable is NEVER executed (DEBUG is disabled)
logger.debug("Debug info", expensive_data=lambda: compute_expensive_data())

# This callable IS executed (ERROR is enabled)
logger.error("Error occurred", context=lambda: gather_error_context())
```

#### Robust JSON Serialization
JSON logger now handles complex Python types without crashing:

```python
from datetime import datetime
from decimal import Decimal
from uuid import uuid4
from pathlib import Path

logger = JSONLogger("app")

# All of these work automatically
logger.info("Event",
    timestamp=datetime.now(),      # → ISO format string
    user_id=uuid4(),                # → string representation
    price=Decimal("19.99"),         # → float
    file_path=Path("/tmp/file"),    # → string
    status=Status.ACTIVE            # → enum value
)
```

#### Thread-Safe Context Updates
All context operations are now thread-safe:

```python
logger = ConsoleLogger("app")

# Safe to call from multiple threads
def worker(worker_id):
    logger.bind(worker_id=worker_id)
    logger.info("Worker started")

threads = [Thread(target=worker, args=(i,)) for i in range(10)]
```

### v0.3.0 Features

#### Flexible Log Levels
Accepts both string and integer log levels for better developer experience:
- Strings: `"DEBUG"`, `"INFO"`, `"WARNING"`, `"ERROR"`, `"CRITICAL"`
- Case-insensitive: `"info"` works the same as `"INFO"`
- Integers: Traditional `logging.DEBUG`, `logging.INFO`, etc.
- Numeric values: `10`, `20`, `30`, `40`, `50`

### Instance-Based
Each logger is a self-contained instance with its own configuration:

```python
def process_data(logger):
    """Accept any logger instance."""
    logger.info("Processing started")
    # ... do work ...
    logger.info("Processing complete")

# Use with different loggers
console = ConsoleLogger("console")
json_log = JSONLogger("json")

process_data(console)  # Outputs to console
process_data(json_log)  # Outputs as JSON
```

### Context Preservation
Permanent context fields appear in every log message:

```python
logger = ConsoleLogger(
    name="worker",
    context={
        "worker_id": "w-1",
        "datacenter": "us-east-1"
    }
)

# Every log includes worker_id and datacenter
logger.info("Task started")
logger.error("Task failed")
```

### Zero Dependencies
Built entirely on Python's standard `logging` module - no external dependencies required for core functionality.

## Migration from Traditional Logging

```python
# Traditional Python logging (global)
import logging
logging.info("Message")

# ff-logger (instance-based)
from ff_logger import ConsoleLogger
logger = ConsoleLogger("app")
logger.info("Message")
```

## Advanced Usage

### Flexible Log Levels

```python
# All of these work now (v0.3.0+):
logger1 = ConsoleLogger("app", level="DEBUG")     # String
logger2 = ConsoleLogger("app", level="info")      # Case-insensitive
logger3 = ConsoleLogger("app", level=logging.INFO) # Traditional int
logger4 = ConsoleLogger("app", level=20)          # Numeric value
logger5 = ConsoleLogger("app")                    # Default: "DEBUG"

# Supported string levels:
# "DEBUG", "INFO", "WARNING"/"WARN", "ERROR", "CRITICAL"
```

### Exception Logging

```python
try:
    risky_operation()
except Exception:
    logger.exception("Operation failed")
    # Automatically includes full traceback
```

### Reserved Fields

Python's logging module reserves 23+ field names for LogRecord internals. If you use these as context fields in log calls, they're automatically prefixed with `x_` to prevent conflicts:

```python
# Constructor 'name' parameter - this works as expected
logger = ConsoleLogger("my_app")  # ✅ Sets logger name

# Log method 'name' kwarg - automatically prefixed to avoid conflict
logger.info("Message", name="custom")  # Becomes x_name="custom"

# Other reserved fields also prefixed
logger.info("Event",
    module="auth",      # Becomes x_module="auth"
    process="worker",   # Becomes x_process="worker"
    thread="t-1"        # Becomes x_thread="t-1"
)
```

**Reserved fields include:** `name`, `module`, `pathname`, `funcName`, `process`, `thread`, `levelname`, `msg`, `args`, and 15+ more. See [Python logging documentation](https://docs.python.org/3/library/logging.html#logrecord-attributes) for the complete list.

**Why?** These fields are used internally by Python's LogRecord class. Overwriting them would cause crashes like "Attempt to overwrite 'name' in LogRecord".

## Testing

Use `NullLogger` in tests for zero overhead:

```python
def test_my_function():
    # Option 1: Pass the class directly
    result = my_function(logger=NullLogger)  # No logging output
    assert result == expected
    
    # Option 2: Functions with NullLogger as default
    def my_function(data, logger=NullLogger):
        logger.info("Processing: %s", data)
        return process(data)
    
    # In tests, just call without logger parameter
    result = my_function(test_data)  # Silent by default
```

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request to the [GitLab repository](https://gitlab.com/fenixflow/fenix-packages).

## License

MIT License - see [LICENSE](LICENSE) file for details.

Copyright (c) 2024 Ben Moag / Fenixflow

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "ff-logger",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.10",
    "maintainer_email": "Fenixflow Team <dev@fenixflow.com>",
    "keywords": "logging, structured-logging, json-logging, scoped-logger, instance-logger, fenixflow",
    "author": null,
    "author_email": "Ben Moag <dev@fenixflow.com>",
    "download_url": "https://files.pythonhosted.org/packages/0e/1a/9e4606dcbddf1e120fee373b5bbfbac85ac4ccfaff33319b96c403573fc2/ff_logger-0.4.1.tar.gz",
    "platform": null,
    "description": "# ff-logger\n\n[![PyPI version](https://badge.fury.io/py/ff-logger.svg)](https://badge.fury.io/py/ff-logger)\n[![Python Support](https://img.shields.io/pypi/pyversions/ff-logger.svg)](https://pypi.org/project/ff-logger/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\nA scoped, instance-based logging package for Fenixflow applications. Unlike traditional Python logging which uses a global configuration, ff-logger provides self-contained logger instances that can be passed around as objects, with support for context binding and multiple output formats.\n\nCreated by **Ben Moag** at **[Fenixflow](https://fenixflow.com)**\n\n## Quick Start\n\n### Installation\n\n#### From PyPI (when published)\n```bash\npip install ff-logger\n```\n\n#### From GitLab (current)\n```bash\npip install git+https://gitlab.com/fenixflow/fenix-packages.git#subdirectory=ff-logger\n```\n\n### Basic Usage\n\n```python\nfrom ff_logger import ConsoleLogger\nimport logging\n\n# Create a logger instance with permanent context\nlogger = ConsoleLogger(\n    name=\"my_app\",\n    level=\"INFO\",  # Can use strings now! (or logging.INFO)\n    context={\"service\": \"api\", \"environment\": \"production\"}\n)\n\n# Log messages with the permanent context\nlogger.info(\"Application started\")\n# Output: [2025-08-20 10:00:00] INFO [my_app] Application started | service=\"api\" environment=\"production\"\n\n# Add runtime context with kwargs\nlogger.info(\"Request processed\", request_id=\"req-123\", duration=45)\n# Output includes both permanent and runtime context\n```\n\n### Context Binding\n\nAdd permanent context fields to your logger instance:\n\n```python\n# Add context that will appear in all subsequent logs\nlogger.bind(\n    request_id=\"req-456\",\n    user_id=789,\n    ip=\"192.168.1.1\"\n)\n\n# All messages now include the bound context\nlogger.info(\"Processing payment\")\nlogger.error(\"Payment failed\", error_code=\"CARD_DECLINED\")\n\n# bind() returns self for chaining\nlogger.bind(session_id=\"xyz\").info(\"Session started\")\n```\n\n**Note:** As of v0.3.0, `bind()` modifies the logger instance in place rather than creating a new one. This is cleaner and more intuitive. The method validates that fields are not reserved and values are JSON-serializable.\n\n## Logger Types\n\n### ConsoleLogger\nOutputs colored, human-readable logs to console:\n\n```python\nfrom ff_logger import ConsoleLogger\n\nlogger = ConsoleLogger(\n    name=\"app\",\n    level=\"INFO\",  # String or int (logging.INFO)\n    colors=True,  # Enable colored output\n    show_hostname=False  # Optional hostname in logs\n)\n```\n\n### JSONLogger\nOutputs structured JSON lines, perfect for log aggregation:\n\n```python\nfrom ff_logger import JSONLogger\n\nlogger = JSONLogger(\n    name=\"app\",\n    level=\"WARNING\",  # String or int levels supported\n    show_hostname=True,\n    include_timestamp=True\n)\n\nlogger.info(\"Event occurred\", event_type=\"user_login\", user_id=123)\n# Output: {\"level\":\"INFO\",\"logger\":\"app\",\"message\":\"Event occurred\",\"timestamp\":\"2025-08-20T10:00:00Z\",\"event_type\":\"user_login\",\"user_id\":123,...}\n```\n\n### FileLogger\nWrites to files with rotation support:\n\n```python\nfrom ff_logger import FileLogger\n\nlogger = FileLogger(\n    name=\"app\",\n    filename=\"/var/log/app.log\",\n    rotation_type=\"size\",  # \"size\", \"time\", or \"none\"\n    max_bytes=10*1024*1024,  # 10MB\n    backup_count=5\n)\n```\n\n### NullLogger\nZero-cost logger for testing or when logging is disabled:\n\n```python\nfrom ff_logger import NullLogger\n\n# Preferred: Use directly as a class (no instantiation needed)\nNullLogger.info(\"This does nothing\")  # No-op\nNullLogger.debug(\"Debug message\")     # No-op\n\n# As a default parameter (perfect for dependency injection)\ndef process_data(data, logger=NullLogger):\n    logger.info(\"Processing data: %s\", data)\n    return data * 2\n\n# Call without providing a logger\nresult = process_data([1, 2, 3])\n\n# Backward compatibility: Can still instantiate if needed\nlogger = NullLogger()  # All parameters are optional\nlogger.info(\"This also does nothing\")\n```\n\n### DatabaseLogger\nWrites logs to a database table (requires ff-storage):\n\n```python\nfrom ff_logger import DatabaseLogger\nfrom ff_storage.db.postgres import PostgresPool\n\ndb = PostgresPool(...)\nlogger = DatabaseLogger(\n    name=\"app\",\n    db_connection=db,\n    table_name=\"logs\",\n    schema=\"public\"\n)\n```\n\n## Key Features\n\n### v0.4.0 Features\n\n#### Temporary Context Manager\nUse the `temp_context()` context manager to add temporary fields that are automatically removed:\n\n```python\nlogger = ConsoleLogger(\"app\")\n\nwith logger.temp_context(request_id=\"123\", user_id=456):\n    logger.info(\"Processing request\")  # Includes request_id and user_id\n    logger.info(\"Request complete\")    # Still includes the fields\n\n# Fields automatically removed after context\nlogger.info(\"Next request\")  # request_id and user_id no longer present\n```\n\n#### Lazy Evaluation for Performance\nPass callables as kwargs to defer expensive computations until needed:\n\n```python\nlogger = ConsoleLogger(\"app\", level=\"ERROR\")  # Only ERROR and above\n\n# This callable is NEVER executed (DEBUG is disabled)\nlogger.debug(\"Debug info\", expensive_data=lambda: compute_expensive_data())\n\n# This callable IS executed (ERROR is enabled)\nlogger.error(\"Error occurred\", context=lambda: gather_error_context())\n```\n\n#### Robust JSON Serialization\nJSON logger now handles complex Python types without crashing:\n\n```python\nfrom datetime import datetime\nfrom decimal import Decimal\nfrom uuid import uuid4\nfrom pathlib import Path\n\nlogger = JSONLogger(\"app\")\n\n# All of these work automatically\nlogger.info(\"Event\",\n    timestamp=datetime.now(),      # \u2192 ISO format string\n    user_id=uuid4(),                # \u2192 string representation\n    price=Decimal(\"19.99\"),         # \u2192 float\n    file_path=Path(\"/tmp/file\"),    # \u2192 string\n    status=Status.ACTIVE            # \u2192 enum value\n)\n```\n\n#### Thread-Safe Context Updates\nAll context operations are now thread-safe:\n\n```python\nlogger = ConsoleLogger(\"app\")\n\n# Safe to call from multiple threads\ndef worker(worker_id):\n    logger.bind(worker_id=worker_id)\n    logger.info(\"Worker started\")\n\nthreads = [Thread(target=worker, args=(i,)) for i in range(10)]\n```\n\n### v0.3.0 Features\n\n#### Flexible Log Levels\nAccepts both string and integer log levels for better developer experience:\n- Strings: `\"DEBUG\"`, `\"INFO\"`, `\"WARNING\"`, `\"ERROR\"`, `\"CRITICAL\"`\n- Case-insensitive: `\"info\"` works the same as `\"INFO\"`\n- Integers: Traditional `logging.DEBUG`, `logging.INFO`, etc.\n- Numeric values: `10`, `20`, `30`, `40`, `50`\n\n### Instance-Based\nEach logger is a self-contained instance with its own configuration:\n\n```python\ndef process_data(logger):\n    \"\"\"Accept any logger instance.\"\"\"\n    logger.info(\"Processing started\")\n    # ... do work ...\n    logger.info(\"Processing complete\")\n\n# Use with different loggers\nconsole = ConsoleLogger(\"console\")\njson_log = JSONLogger(\"json\")\n\nprocess_data(console)  # Outputs to console\nprocess_data(json_log)  # Outputs as JSON\n```\n\n### Context Preservation\nPermanent context fields appear in every log message:\n\n```python\nlogger = ConsoleLogger(\n    name=\"worker\",\n    context={\n        \"worker_id\": \"w-1\",\n        \"datacenter\": \"us-east-1\"\n    }\n)\n\n# Every log includes worker_id and datacenter\nlogger.info(\"Task started\")\nlogger.error(\"Task failed\")\n```\n\n### Zero Dependencies\nBuilt entirely on Python's standard `logging` module - no external dependencies required for core functionality.\n\n## Migration from Traditional Logging\n\n```python\n# Traditional Python logging (global)\nimport logging\nlogging.info(\"Message\")\n\n# ff-logger (instance-based)\nfrom ff_logger import ConsoleLogger\nlogger = ConsoleLogger(\"app\")\nlogger.info(\"Message\")\n```\n\n## Advanced Usage\n\n### Flexible Log Levels\n\n```python\n# All of these work now (v0.3.0+):\nlogger1 = ConsoleLogger(\"app\", level=\"DEBUG\")     # String\nlogger2 = ConsoleLogger(\"app\", level=\"info\")      # Case-insensitive\nlogger3 = ConsoleLogger(\"app\", level=logging.INFO) # Traditional int\nlogger4 = ConsoleLogger(\"app\", level=20)          # Numeric value\nlogger5 = ConsoleLogger(\"app\")                    # Default: \"DEBUG\"\n\n# Supported string levels:\n# \"DEBUG\", \"INFO\", \"WARNING\"/\"WARN\", \"ERROR\", \"CRITICAL\"\n```\n\n### Exception Logging\n\n```python\ntry:\n    risky_operation()\nexcept Exception:\n    logger.exception(\"Operation failed\")\n    # Automatically includes full traceback\n```\n\n### Reserved Fields\n\nPython's logging module reserves 23+ field names for LogRecord internals. If you use these as context fields in log calls, they're automatically prefixed with `x_` to prevent conflicts:\n\n```python\n# Constructor 'name' parameter - this works as expected\nlogger = ConsoleLogger(\"my_app\")  # \u2705 Sets logger name\n\n# Log method 'name' kwarg - automatically prefixed to avoid conflict\nlogger.info(\"Message\", name=\"custom\")  # Becomes x_name=\"custom\"\n\n# Other reserved fields also prefixed\nlogger.info(\"Event\",\n    module=\"auth\",      # Becomes x_module=\"auth\"\n    process=\"worker\",   # Becomes x_process=\"worker\"\n    thread=\"t-1\"        # Becomes x_thread=\"t-1\"\n)\n```\n\n**Reserved fields include:** `name`, `module`, `pathname`, `funcName`, `process`, `thread`, `levelname`, `msg`, `args`, and 15+ more. See [Python logging documentation](https://docs.python.org/3/library/logging.html#logrecord-attributes) for the complete list.\n\n**Why?** These fields are used internally by Python's LogRecord class. Overwriting them would cause crashes like \"Attempt to overwrite 'name' in LogRecord\".\n\n## Testing\n\nUse `NullLogger` in tests for zero overhead:\n\n```python\ndef test_my_function():\n    # Option 1: Pass the class directly\n    result = my_function(logger=NullLogger)  # No logging output\n    assert result == expected\n    \n    # Option 2: Functions with NullLogger as default\n    def my_function(data, logger=NullLogger):\n        logger.info(\"Processing: %s\", data)\n        return process(data)\n    \n    # In tests, just call without logger parameter\n    result = my_function(test_data)  # Silent by default\n```\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request to the [GitLab repository](https://gitlab.com/fenixflow/fenix-packages).\n\n## License\n\nMIT License - see [LICENSE](LICENSE) file for details.\n\nCopyright (c) 2024 Ben Moag / Fenixflow\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Fenixflow structured logging package with scoped, instance-based loggers",
    "version": "0.4.1",
    "project_urls": {
        "Bug Tracker": "https://gitlab.com/fenixflow/fenix-packages/-/issues",
        "Changelog": "https://gitlab.com/fenixflow/fenix-packages/-/blob/main/ff-logger/CHANGELOG.md",
        "Documentation": "https://gitlab.com/fenixflow/fenix-packages/-/tree/main/ff-logger",
        "Homepage": "https://fenixflow.com",
        "Repository": "https://gitlab.com/fenixflow/fenix-packages"
    },
    "split_keywords": [
        "logging",
        " structured-logging",
        " json-logging",
        " scoped-logger",
        " instance-logger",
        " fenixflow"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "5bf7cc28219fa6f557de7c4c0977a1c48000762f21069f72d95fa5e73c3ab5f1",
                "md5": "2914ac3b89e77ea28755315f7c5f0449",
                "sha256": "c0b1b1c992aced3ab6c95a2dc1cb3f0d5f00d2019f717a8b799dd136276b88c9"
            },
            "downloads": -1,
            "filename": "ff_logger-0.4.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "2914ac3b89e77ea28755315f7c5f0449",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.10",
            "size": 20067,
            "upload_time": "2025-10-06T19:41:01",
            "upload_time_iso_8601": "2025-10-06T19:41:01.210142Z",
            "url": "https://files.pythonhosted.org/packages/5b/f7/cc28219fa6f557de7c4c0977a1c48000762f21069f72d95fa5e73c3ab5f1/ff_logger-0.4.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "0e1a9e4606dcbddf1e120fee373b5bbfbac85ac4ccfaff33319b96c403573fc2",
                "md5": "f7a58768b39b31223af17d78c7ee35f4",
                "sha256": "43af1b4ee9d77325a645099de654ac39644fc4815e43eeba6f17bac6f9342d5c"
            },
            "downloads": -1,
            "filename": "ff_logger-0.4.1.tar.gz",
            "has_sig": false,
            "md5_digest": "f7a58768b39b31223af17d78c7ee35f4",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.10",
            "size": 29220,
            "upload_time": "2025-10-06T19:41:02",
            "upload_time_iso_8601": "2025-10-06T19:41:02.164212Z",
            "url": "https://files.pythonhosted.org/packages/0e/1a/9e4606dcbddf1e120fee373b5bbfbac85ac4ccfaff33319b96c403573fc2/ff_logger-0.4.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-10-06 19:41:02",
    "github": false,
    "gitlab": true,
    "bitbucket": false,
    "codeberg": false,
    "gitlab_user": "fenixflow",
    "gitlab_project": "fenix-packages",
    "lcname": "ff-logger"
}
        
Elapsed time: 1.77148s