Skip to content

Commit

Permalink
Merge pull request #1 from jieggii/next
Browse files Browse the repository at this point in the history
Next
  • Loading branch information
jieggii authored Jan 4, 2025
2 parents 1df7307 + a0c045e commit 22b8506
Show file tree
Hide file tree
Showing 21 changed files with 1,094 additions and 618 deletions.
94 changes: 61 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,85 @@
## Features
- **Lightweight**: minicfg is a small package with no dependencies.
- **Easy to use**: minicfg provides a simple API to define and populate configurations.
- **Type casting**: minicfg supports type casting for the fields.
- **File attachment**: minicfg supports attaching a file to a field.
- **Documentation**: generate documentation for your configuration.
- **Type casting**: minicfg supports type casting for the fields. You can also define your own casters.
- **File field attachment**: minicfg supports attaching a virtual file field to a field.
- **Prefixing**: minicfg supports prefixing the fields with a custom name prefix.
- **Nested configurations**: minicfg supports nested configurations.
- **Custom providers**: minicfg supports custom providers to populate the configuration from different sources.

## Installation
Just install minicfg using your favorite package manager, for example:

```bash
pip install minicfg
```

## Usage

```python
from minicfg import Minicfg, Field, minicfg_prefix
from minicfg import Minicfg, Field, minicfg_name
from minicfg.caster import IntCaster

@minicfg_prefix("MYSERVICE")
class Env(Minicfg):
@minicfg_prefix("BOT")
class TelegramBot(Minicfg):
# attach_file_field=True will read the value from the file provided in MYSERVICE_BOT_TOKEN_FILE env var
# if no file is provided, it will read the value from MYSERVICE_BOT_TOKEN env var.
TOKEN = Field(attach_file_field=True)

class API1(Minicfg): # <-- API1 class name will be used as a prefix for the fields inside it
API_TOKEN = Field() # API_TOKEN will be read from MYSERVICE_API1_API_TOKEN env var

@minicfg_prefix("MONGO")
class Mongo(Minicfg):
HOST = Field()
PORT = Field(caster=IntCaster()) # PORT will be casted to an integer type


# Populate the configuration from the environment variables:
env = Env.populated()

print(f"Telegram bot token: {env.TelegramBot.TOKEN}")
print(f"API1 token: {env.API1.API_TOKEN}")
print(f"Mongo settings: {env.Mongo.HOST}:{env.Mongo.PORT}")

"""

@minicfg_name("SERVICE")
class MyConfig(Minicfg):
@minicfg_name("DATABASE")
class Database(Minicfg):
HOST = Field(attach_file_field=True, description="database host")
PORT = Field(default=5432, caster=IntCaster(), description="database port")

@minicfg_name("EXTERNAL_API")
class ExternalAPI(Minicfg):
KEY = Field(description="external API key")
USER_ID = Field(caster=IntCaster(), description="external API user ID")


if __name__ == '__main__':
config = MyConfig() # create an instance of the configuration
config.populate() # populate the configuration from the environment variables

print(f"connect to database at {config.Database.HOST}:{config.Database.PORT}")
print(f"external API key: {config.ExternalAPI.KEY}")
print(f"external API user: {config.ExternalAPI.USER_ID}")
```

Try running the script with the following environment variables:
MYSERVICE_BOT_TOKEN=token MYSERVICE_API1_API_TOKEN=token123 MYSERVICE_MONGO_HOST=localhost MYSERVICE_MONGO_PORT=5432
- `SERVICE_DATABASE_HOST=example.com`
- `SERVICE_DATABASE_PORT=5432`
- `SERVICE_EXTERNAL_API_KEY=token`
- `SERVICE_EXTERNAL_API_USER_ID=123`

And you should see the following output:

>>> Telegram bot token: token
>>> API1 token: token123
>>> Mongo settings: localhost:5432
"""
```
connect to database at example.com:5432
external API key: token
external API user: 123
```

> More examples are available [here](/examples).
### Documentation generation
You can use `minicfg` script to generate documentation for your configuration.

For example, `minicifg --format=markdown example.MyConfig` will generate documentation for
the `MyConfig` class above, and it would look like this:

**SERVICE**
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |

**SERVICE_DATABASE**
| Name | Type | Default | Description |
| ---------------------------- | ----- | ------- | ------------------ |
| `SERVICE_DATABASE_HOST` | `str` | N/A | database host |
| `SERVICE_DATABASE_HOST_FILE` | `str` | N/A | database host file |
| `SERVICE_DATABASE_PORT` | `int` | `5432` | database port |


**SERVICE_EXTERNAL_API**
| Name | Type | Default | Description |
| ------------------------------ | ----- | ------- | -------------------- |
| `SERVICE_EXTERNAL_API_KEY` | `str` | N/A | external API key |
| `SERVICE_EXTERNAL_API_USER_ID` | `int` | N/A | external API user ID |
38 changes: 0 additions & 38 deletions examples/attach_file_field.py

This file was deleted.

49 changes: 49 additions & 0 deletions examples/docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""
This example demonstrates how to document the configuration class.
"""

from minicfg import Field, Minicfg, minicfg_name
from minicfg.caster import IntCaster
from minicfg.docs_generator import DocsGenerator
from minicfg.provider import AbstractProvider


class MockProvider(AbstractProvider):
"""
A custom mock provider.
Used to simulate the environment variables.
"""

data = {"DATABASE_HOST": "example.com", "DATABASE_PORT": "5432", "EXTERNAL_API_KEY": "api_key"}

def get(self, key: str) -> str | None:
return self.data.get(key)


class MyConfig(Minicfg):
@minicfg_name("DATABASE")
class Database(Minicfg):
HOST = Field(default="localhost", description="database host")
PORT = Field(caster=IntCaster(), description="database port")

@minicfg_name("EXTERNAL_API")
class ExternalAPI(Minicfg):
KEY = Field(description="external API key")


"""
Try running `python docs.py` or `python -m minicfg docs.MyConfig --format=plaintext` and you should see the following output:
MyConfig
DATABASE
- DATABASE_HOST: str = localhost # database host
- DATABASE_PORT: int # database port
EXTERNAL_API
- EXTERNAL_API_KEY: str # external API key
"""
if __name__ == "__main__":
config = MyConfig() # create a new instance of the config
docs_generator = DocsGenerator(config) # create a new instance of the docs generator
print(docs_generator.as_plaintext()) # print the documentation as plain text
51 changes: 51 additions & 0 deletions examples/file_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""
This example demonstrates how to attach a file field to another field.
"""

from minicfg import Field, Minicfg
from minicfg.provider import AbstractProvider


class MyProvider(AbstractProvider):
"""
A provider that reads the hostname from the /etc/hostname file.
"""

data = {
"DATABASE_HOST_FILE": "/etc/hostname",
}

def get(self, key: str) -> str | None:
return self.data.get(key)


class MyConfig(Minicfg):
"""
My configuration class.
"""

"""
A virtual DATABASE_HOST_FILE field will be also created and attached to the DATABASE_HOST field.
- If only DATABASE_HOST_FILE is provided, the field value will be read from the file.
- If only DATABASE_HOST is provided, the field value will be used directly from it.
- If both DATABASE_HOST_FILE and DATABASE_HOST are provided, the value of DATABASE_HOST will be used.
- If none of them are provided, the FieldValueNotProvidedError will be raised.
"""
DATABASE_HOST = Field(attach_file_field=True)


"""
Try running `python file_field.py` and you should see the following output:
>>> config.DATABASE_HOST='<your hostname>'
If you don't have a hostname file, the FileNotFound exception will be raised.
"""
if __name__ == "__main__":
provider = MyProvider() # create a new instance of the custom provider

config = MyConfig() # create a new instance of the config
config.populate(provider) # populate the config using the custom provider

print(f"{config.DATABASE_HOST=}")
# >>> config.DATABASE_HOST='<your hostname>'
59 changes: 59 additions & 0 deletions examples/nesting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""
This example demonstrates how to use prefixes with minicfg.
"""

from minicfg import Field, Minicfg, minicfg_name
from minicfg.caster import IntCaster
from minicfg.provider import AbstractProvider


class MockProvider(AbstractProvider):
"""
A custom mock provider.
Used to simulate the environment variables.
"""

data = {
"SERVICE_DATABASE_HOST": "example.com",
"SERVICE_EXTERNAL_API_KEY": "api_key",
"SERVICE_EXTERNAL_API_USER_ID": "user123",
}

def get(self, key: str) -> str | None:
return self.data.get(key)


@minicfg_name("SERVICE") # <-- The prefix for the main config (will be inherited by nested configs).
class MyConfig(Minicfg):
"""
My configuration class.
"""

@minicfg_name("DATABASE") # <-- The prefix for the nested config.
class Database(Minicfg):
HOST: str = Field()
PORT: int = Field(default=5432, caster=IntCaster())

@minicfg_name("EXTERNAL_API") # <-- The prefix for the nested config.
class ExternalAPI(Minicfg):
KEY: str = Field(description="external API key")
USER_ID: str = Field(description="external API user ID")


"""
Try running `python nesting.py` and you should see the following output:
>>> config.Database.HOST='example.com'
>>> config.Database.PORT=5432
>>> config.ExternalAPI.KEY='api_key'
>>> config.ExternalAPI.USER_ID='user123'
"""
if __name__ == "__main__":
provider = MockProvider() # create a new instance of the custom provider

config = MyConfig() # create a new instance of the config
config.populate(provider) # populate the config using the custom provider

print(f"{config.Database.HOST=}")
print(f"{config.Database.PORT=}")
print(f"{config.ExternalAPI.KEY=}")
print(f"{config.ExternalAPI.USER_ID=}")
64 changes: 0 additions & 64 deletions examples/prefixes.py

This file was deleted.

Loading

0 comments on commit 22b8506

Please sign in to comment.