Metadata-Version: 2.4
Name: universal-common
Version: 1.3.0
Summary: Library that extends the Python base class library with utility functions.
Author-email: Andrew Ong <ong.andrew@gmail.com>
License-File: LICENSE
Classifier: Development Status :: 5 - Production/Stable
Classifier: License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# universal-common

A library that extends the Python base class library with utility functions and types, inspired by the .NET base class library and language features - without needing full feature parity, just enough to make everyday Python a little saner.

## Features

- Null-coalescing (`coalesce`) and null/whitespace string checks (`is_null_or_empty`, `is_null_or_white_space`)
- LINQ-style iteration helpers (`first`, `first_or_default`, `last`, `last_or_default`), available both as free functions and as methods on `List`
- `Dictionary`: a `dict` subclass that also supports attribute access
- `event`/`BoundEvent`: a C#-style event pattern with `+=`/`-=` subscription
- `Stopwatch`: a simple elapsed-time measurer, usable as a context manager
- `MediaType`: parsing and building MIME media type strings (e.g. `"application/json; charset=utf-8"`)
- `SemanticVersion`: dependency-free Semantic Versioning 2.0.0 parsing and comparison
- `SqlConnectionStringBuilder`: a minimal SQL connection string builder/parser

## Installation

Install the package from PyPI using pip:

```bash
pip install universal-common
```

## Usage

### Null coalescing and string checks

```python
from universal_common import coalesce, is_null_or_empty, is_null_or_white_space

coalesce(None, None, 3)          # 3 - first non-None value
is_null_or_empty(None)           # True
is_null_or_empty("")             # True
is_null_or_white_space("   ")    # True - unlike `if not s:`, this catches whitespace-only strings
```

### LINQ-style iteration helpers

```python
from universal_common import first, first_or_default, last, last_or_default, List

numbers = [1, 2, 5, 4]
first(numbers)                          # 1
first(numbers, lambda x: x > 3)         # 5
last(numbers)                           # 4
last_or_default(numbers, lambda x: x > 10)  # None, instead of raising

# The same methods are available directly on List:
items = List([1, 2, 5, 4])
items.first_or_default(lambda x: x > 3)  # 5
items.last()                             # 4
```

`first`/`last` raise `ValueError` when nothing matches; `first_or_default`/`last_or_default` return `None` instead.

### Attribute-accessible dictionaries

```python
from universal_common import Dictionary

settings = Dictionary({"name": "Ada"})
settings.name        # "Ada"
settings["name"]     # "Ada"
settings.age = 30    # same as settings["age"] = 30
settings.missing     # None, does not raise
```

### C#-style events

```python
from universal_common import event

class Button:
    @event
    def clicked(self):
        """Raised when the button is clicked."""

button = Button()
button.clicked += lambda: print("clicked!")
button.clicked()  # invokes every subscribed handler
```

If more than one handler raises, every handler still runs; the first exception is raised, and any additional ones are attached to it as `.other_exceptions` rather than silently discarded.

### Timing

```python
from universal_common import Stopwatch

with Stopwatch() as stopwatch:
    do_work()
print(stopwatch.elapsed, "seconds")

# Or standalone:
stopwatch = Stopwatch.start_new()
do_work()
stopwatch.stop()
print(stopwatch.elapsed_milliseconds, "ms")
```

### Media types

```python
from universal_common import MediaType

media_type = MediaType.parse("application/json; charset=utf-8")
media_type.full_type   # "application/json"
media_type.charset     # "utf-8"
str(media_type)        # "application/json; charset=utf-8"
```

### Semantic versioning

```python
from universal_common import SemanticVersion

version = SemanticVersion.parse("1.2.3-beta.1+build.5")
version.major, version.minor, version.patch  # (1, 2, 3)

SemanticVersion.parse("1.0.0-alpha") < SemanticVersion.parse("1.0.0")  # True
sorted([SemanticVersion.parse(v) for v in ["2.0.0", "1.0.0", "1.5.0"]])
```

Comparison follows the semver 2.0.0 precedence rules exactly: numeric identifiers sort before alphanumeric ones, a version without a prerelease outranks one with a prerelease at the same `major.minor.patch`, and build metadata (`+...`) is ignored for comparison.

### SQL connection strings

```python
from universal_common import SqlConnectionStringBuilder

builder = SqlConnectionStringBuilder()
builder.server = "localhost"
builder.database = "app"
builder.user_id = "admin"
builder.password = "hunter2"
str(builder)  # "Server=localhost;Database=app;Uid=admin;Pwd=hunter2"

parsed = SqlConnectionStringBuilder.parse("Server=localhost;Database=app")
parsed.server    # "localhost"
```

## License

CC0 1.0 Universal (Public Domain Dedication).
