Metadata-Version: 2.4
Name: secsie-conf
Version: 4.0.0
Summary: A small library for parsing configuration files
Home-page: https://github.com/noahbroyles/secsie-conf
Author: Noah Broyles
Author-email: noah@javamate.net
License: MIT
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license
Dynamic: license-file
Dynamic: requires-python
Dynamic: summary

# secsie-conf

[![PyPi Deployment](https://github.com/noahbroyles/secsie-conf/actions/workflows/deploy-to-pypi.yaml/badge.svg?event=release)](https://github.com/noahbroyles/secsie-conf/actions/workflows/deploy-to-pypi.yaml)
[![Unit Tests](https://github.com/noahbroyles/secsie-conf/actions/workflows/unit-tests.yaml/badge.svg)](https://github.com/noahbroyles/secsie-conf/actions/workflows/unit-tests.yaml)

```console
pip3 install secsie-conf
```
Secsie is a configuration language parser for Python, made for speed and beauty. Instead of writing config files in JSON (don't get me wrong, JSON is *FAR* better than a lot of other things you could use (cough cough XML)), you can save time writing your config files in `secsie`.  
The `secsie` language format is very similar to `ini`, except just a little better. You can use `secsie-conf` to read `.ini` files into Python `dict`s. `secsie-conf` can write `.ini` files too, see [Write an `.ini` file](#write-an-ini-file).  


### Advantages over JSON:
- easier to read
- faster and more natural to write
- no special syntax required for strings vs ints, floats, bools, etc.
- file size usually smaller than readable JSON


## Secsie Language Constructs
These are the rules of the secsie config language:
1. Comment lines begin with `#` or `;`, but inline comments can only begin with the octothorpe (`#`) and _must_ have a space preceding them.
2. Whitespace is ignored everywhere except in key names and section tag names.
3. A config file consists of sections and attributes(keys and values).
4. A section ends where the next section begins. Attributes declared before sections begin are valid.
5. To begin a section use the following syntax:
```ini
[section1]
# attribute lines in this section follow
```
6. The syntax for an attribute line is:
```ini
key = value
```
7. Spaces are not allowed in key names or section tags. Only `a-z`(case insensitive), `0-9`, `_`, and `-` are allowed in section tag names, while other special characters *are* allowed in key names.
8. Values can contain any character, including `#`. A `#` only starts an inline comment when it is preceded by a space, so `password = p#ss` is fine but `password = p #ss` is cut off at the space. Leading and trailing whitespace is removed. String values separated by commas are interpreted as lists.

## INI
`secsie-conf` can be used to read `.ini` files, as long as the mode `ini` is specifed to the parser. The rules of interpretation for `.ini` files vary slightly.
### Differences:
- Section names are allowed to contain spaces
- quoted strings are valid, but the quotes are removed (there is no need to quote string in `secsie` ;). A quoted value is always kept as a string, so `"42"` stays a string and `"a,b"` is not split into a list  
- Lists in `.ini` files can have a trailing comma with no effect, but a trailing comma in `secsie` will create a blank string  
`secsie-conf` can also write the `ini` dialect described here, see [Write an `.ini` file](#write-an-ini-file).

## Valid values
Secsie supports strings, ints, floats, null types, booleans, and lists. Most of these types can be written out by themselves and will automatically be converted to the appropriate native type. Lists require comma separation.
### Strings:
```ini
# Spaces are allowed in string value
string_key = whatever value you want
```
### Ints:
```ini
numb = 42  # Automatically converted to int when parsed
```
### Floats:
```ini
pi = 3.14159265  # Automatically converted to float when parsed
```
### Null type (`None`):
```ini
# 'null' is used as the NoneType (case insensitive)
nothing = null
```
### Booleans:
```ini
# True and yes are truthy values (case insensitive)
truth = true
truth2 = True
truth3 = yes
# False and no are falsy values (case insensitive)
untruth = falsE
untruth2 = False
untruth3 = no
```
### Lists:
```ini
# To interpret a value as a list, you must separate the list values with a comma.
list = this, that, the other
```

## Limitations
`secsie` has no quoting. A value is whatever you write, and its type is inferred from it, so quotes are just ordinary characters (`name = "Bob"` gives `'"Bob"'`, quotes included). This keeps the language simple, but it means some values can't be written as strings:
- Digits that must stay text, like an account number or zip code with a leading zero. `090192837418` is read as the int `90192837418`.
- Words that are special values, such as `true`, `no`, `yes`, and `null`.
- Strings containing a comma, which are read as lists.
- Strings containing a space followed by `#`, which start an inline comment.

If your program needs one of these as a string, convert it after reading, for example `str(config['zip']).zfill(5)`. Only `ini` mode understands quotes (see above), since it exists to read existing `.ini` files. If you need to *write* values like these, [`ini` mode](#write-an-ini-file) quotes them for you.

## Examples
`examples/valid.secsie.conf`:
```ini
; This is an example of a valid secsie file

before_section = totally okay

# Whitespace don't matter

; Here are examples of how types are interpreted(no keywords are off limits!)
[special_values]
    int = 42
    float = 269.887
    truth = yes
    falsehood = no
    true = true
    ; I don't encourage this but it's valid ;)
    false = FaLSe
    
    # this following will be interpreted as a list
    coolKids = jim, bill, bob, alice, 42
    
    # Null value
    nah = Null

[anotherSection]
    # The indent here is optional, included for readability
    sections = are amazing

```
Parse result:
```python
{'before_section': 'totally okay', 'special_values': {'int': 42, 'float': 269.887, 'truth': True, 'falsehood': False, 'true': True, 'false': False, 'coolKids': ['jim', 'bill', 'bob', 'alice', 42], 'nah': None}, 'anotherSection': {'sections': 'are amazing'}}
```

## Module Usage
### Read a `secsie` config file as a `dict`:
```python
import json
import secsie

# To get a dict from a config file
config = secsie.parse_config_file('examples/valid.secsie.conf')
print(json.dumps(config, indent=2))  # For prettyness
```
Result:
```json
{
  "before_section": "totally okay",
  "special_values": {
    "int": 42,
    "float": 269.887,
    "truth": true,
    "falsehood": false,
    "true": true,
    "false": false,
    "coolKids": [
      "jim",
      "bill",
      "bob",
      "alice",
      42
    ],
    "nah": null
  },
  "anotherSection": {
    "sections": "are amazing"
  }
}
```
### Read an `.ini` file:
```python
import json
import secsie

# This might not work...
config = secsie.parse_config_file('examples/php.ini')
```
Result:
```console
Traceback (most recent call last):
  ...
  File "<string>", line 955
secsie.InvalidSyntax: Invalid syntax on line 955: "[CLI Server]" - bad section descriptor or value assignment (line 955)
```
We can see that this up and broke. WTF?! Actually, it's okay. Spaces aren't allowed in `secsie` section names, remember? When reading an `.ini` file, we need to pass the argument `mode='ini'` to the `parse_config_file` function, like this:  
```python
config = secsie.parse_config_file('examples/php.ini', mode='ini')
print(json.dumps(config, indent=2))
```
```json
{
  "PHP": {
    "engine": "On",
    "short_open_tag": "Off",
    "precision": 14,
    "output_buffering": 4096,
    "zlib.output_compression": "Off",
    "implicit_flush": "Off",
    "unserialize_callback_func": "",
    "serialize_precision": -1,
    "disable_functions": [
      "pcntl_alarm",
      "pcntl_fork",
      "pcntl_waitpid",
      "pcntl_wait",
      "pcntl_wifexited",
      "pcntl_wifstopped",
      "pcntl_wifsignaled",
      "pcntl_wifcontinued",
      "pcntl_wexitstatus",
      "pcntl_wtermsig",
      "pcntl_wstopsig",
      "pcntl_signal",
      "pcntl_signal_get_handler",
      "pcntl_signal_dispatch",
      "pcntl_get_last_error",
      "pcntl_strerror",
      "pcntl_sigprocmask",
      "pcntl_sigwaitinfo",
      "pcntl_sigtimedwait",
      "pcntl_exec",
      "pcntl_getpriority",
      "pcntl_setpriority",
      "pcntl_async_signals",
      "pcntl_unshare"
    ],
    "disable_classes": "",
    "zend.enable_gc": "On",
    "zend.exception_ignore_args": "On",
    "expose_php": "Off",
    "max_execution_time": 30,
    "max_input_time": 60,
    "memory_limit": "128M",
    "error_reporting": "E_ALL & ~E_DEPRECATED & ~E_STRICT",
    "display_errors": "Off",
    "display_startup_errors": "Off",
    "log_errors": "On",
    "log_errors_max_len": 1024,
    "ignore_repeated_errors": "Off",
    "ignore_repeated_source": "Off",
    "report_memleaks": "On",
    "variables_order": "GPCS",
    "request_order": "GP",
    "register_argc_argv": "Off",
    "auto_globals_jit": "On",
    "post_max_size": "8M",
    "auto_prepend_file": "",
    "auto_append_file": "",
    "default_mimetype": "text/html",
    "default_charset": "UTF-8",
    "doc_root": "",
    "user_dir": "",
    "enable_dl": "Off",
    "file_uploads": "On",
    "upload_max_filesize": "2M",
    "max_file_uploads": 20,
    "allow_url_fopen": "On",
    "allow_url_include": "Off",
    "default_socket_timeout": 60
  },
  "CLI Server": {
    "cli_server.color": "On"
  },
  "Pdo_mysql": {
    "pdo_mysql.default_socket": ""
  },
  "mail function": {
    "SMTP": "localhost",
    "smtp_port": 25,
    "mail.add_x_header": "Off"
  },
  "ODBC": {
    "odbc.allow_persistent": "On",
    "odbc.check_persistent": "On",
    "odbc.max_persistent": -1,
    "odbc.max_links": -1,
    "odbc.defaultlrl": 4096,
    "odbc.defaultbinmode": 1
  },
  "MySQLi": {
    "mysqli.max_persistent": -1,
    "mysqli.allow_persistent": "On",
    "mysqli.max_links": -1,
    "mysqli.default_port": 3306,
    "mysqli.default_socket": "",
    "mysqli.default_host": "",
    "mysqli.default_user": "",
    "mysqli.default_pw": "",
    "mysqli.reconnect": "Off"
  },
  "mysqlnd": {
    "mysqlnd.collect_statistics": "On",
    "mysqlnd.collect_memory_statistics": "Off"
  },
  "PostgreSQL": {
    "pgsql.allow_persistent": "On",
    "pgsql.auto_reset_persistent": "Off",
    "pgsql.max_persistent": -1,
    "pgsql.max_links": -1,
    "pgsql.ignore_notice": 0,
    "pgsql.log_notice": 0
  },
  "bcmath": {
    "bcmath.scale": 0
  },
  "Session": {
    "session.save_handler": "files",
    "session.use_strict_mode": 0,
    "session.use_cookies": 1,
    "session.use_only_cookies": 1,
    "session.name": "PHPSESSID",
    "session.auto_start": 0,
    "session.cookie_lifetime": 0,
    "session.cookie_path": "/",
    "session.cookie_domain": "",
    "session.cookie_httponly": "",
    "session.cookie_samesite": "",
    "session.serialize_handler": "php",
    "session.gc_probability": 0,
    "session.gc_divisor": 1000,
    "session.gc_maxlifetime": 1440,
    "session.referer_check": "",
    "session.cache_limiter": "nocache",
    "session.cache_expire": 180,
    "session.use_trans_sid": 0,
    "session.sid_length": 26,
    "session.trans_sid_tags": "a=href,area=href,frame=src,form=",
    "session.sid_bits_per_character": 5
  },
  "Assertion": {
    "zend.assertions": -1
  },
  "Tidy": {
    "tidy.clean_output": "Off"
  },
  "soap": {
    "soap.wsdl_cache_enabled": 1,
    "soap.wsdl_cache_dir": "/tmp",
    "soap.wsdl_cache_ttl": 86400,
    "soap.wsdl_cache_limit": 5
  },
  "ldap": {
    "ldap.max_links": -1
  }
}
```
That **^**'s a whole PHP configuration file converted to some <s>sexy</s> secsie JSON!  
### Write a `secsie` file:
Now that we have the `php.ini` contents stored in `config`, let's output it in `secsie`!
```python
secsie.generate_config_file(config, output_file='examples/php_ini.secsie.conf')
```
Output (`examples/php_ini.secsie.conf`):
```ini
# php_ini.secsie.conf auto-generated by secsie

[PHP]
	engine = On
	short_open_tag = Off
	precision = 14
	output_buffering = 4096
	zlib.output_compression = Off
	implicit_flush = Off
;	unserialize_callback_func = 
	serialize_precision = -1
	disable_functions = pcntl_alarm, pcntl_fork, pcntl_waitpid, pcntl_wait, pcntl_wifexited, pcntl_wifstopped, pcntl_wifsignaled, pcntl_wifcontinued, pcntl_wexitstatus, pcntl_wtermsig, pcntl_wstopsig, pcntl_signal, pcntl_signal_get_handler, pcntl_signal_dispatch, pcntl_get_last_error, pcntl_strerror, pcntl_sigprocmask, pcntl_sigwaitinfo, pcntl_sigtimedwait, pcntl_exec, pcntl_getpriority, pcntl_setpriority, pcntl_async_signals, pcntl_unshare
;	disable_classes = 
	zend.enable_gc = On
	zend.exception_ignore_args = On
	expose_php = Off
	max_execution_time = 30
	max_input_time = 60
	memory_limit = 128M
	error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
	display_errors = Off
	display_startup_errors = Off
	log_errors = On
	log_errors_max_len = 1024
	ignore_repeated_errors = Off
	ignore_repeated_source = Off
	report_memleaks = On
	variables_order = GPCS
	request_order = GP
	register_argc_argv = Off
	auto_globals_jit = On
	post_max_size = 8M
;	auto_prepend_file = 
;	auto_append_file = 
	default_mimetype = text/html
	default_charset = UTF-8
;	doc_root = 
;	user_dir = 
	enable_dl = Off
	file_uploads = On
	upload_max_filesize = 2M
	max_file_uploads = 20
	allow_url_fopen = On
	allow_url_include = Off
	default_socket_timeout = 60


[CLIServer]
	cli_server.color = On


[Pdo_mysql]
;	pdo_mysql.default_socket = 


[mailfunction]
	SMTP = localhost
	smtp_port = 25
	mail.add_x_header = Off


[ODBC]
	odbc.allow_persistent = On
	odbc.check_persistent = On
	odbc.max_persistent = -1
	odbc.max_links = -1
	odbc.defaultlrl = 4096
	odbc.defaultbinmode = 1


[MySQLi]
	mysqli.max_persistent = -1
	mysqli.allow_persistent = On
	mysqli.max_links = -1
	mysqli.default_port = 3306
;	mysqli.default_socket = 
;	mysqli.default_host = 
;	mysqli.default_user = 
;	mysqli.default_pw = 
	mysqli.reconnect = Off


[mysqlnd]
	mysqlnd.collect_statistics = On
	mysqlnd.collect_memory_statistics = Off


[PostgreSQL]
	pgsql.allow_persistent = On
	pgsql.auto_reset_persistent = Off
	pgsql.max_persistent = -1
	pgsql.max_links = -1
	pgsql.ignore_notice = 0
	pgsql.log_notice = 0


[bcmath]
	bcmath.scale = 0


[Session]
	session.save_handler = files
	session.use_strict_mode = 0
	session.use_cookies = 1
	session.use_only_cookies = 1
	session.name = PHPSESSID
	session.auto_start = 0
	session.cookie_lifetime = 0
	session.cookie_path = /
;	session.cookie_domain = 
;	session.cookie_httponly = 
;	session.cookie_samesite = 
	session.serialize_handler = php
	session.gc_probability = 0
	session.gc_divisor = 1000
	session.gc_maxlifetime = 1440
;	session.referer_check = 
	session.cache_limiter = nocache
	session.cache_expire = 180
	session.use_trans_sid = 0
	session.sid_length = 26
	session.trans_sid_tags = a=href,area=href,frame=src,form=
	session.sid_bits_per_character = 5


[Assertion]
	zend.assertions = -1


[Tidy]
	tidy.clean_output = Off


[soap]
	soap.wsdl_cache_enabled = 1
	soap.wsdl_cache_dir = /tmp
	soap.wsdl_cache_ttl = 86400
	soap.wsdl_cache_limit = 5


[ldap]
	ldap.max_links = -1
```
You should notice 2 things: 
1. Keys and value assignments are separated by an equals sign with a space ON BOTH SIDES! `key = value`, **NOT** `key=value`. That is ugly and lazy. This ain't minified JS, son. [They make things for that...](https://www.amazon.com/dp/B089C3TZL9)
2. Blank values were commented out. If you disagree with that, *MAKE 'EM NULL*! `key = ` doesn't say anything.

Also note that `trans_sid_tags` went in as a quoted ini string, and came out as a plain `a=href,area=href,frame=src,form=`. Since a comma means "list" in `secsie`, this will read back as a list. See the limits below.

### Change the header
`generate_config_file` starts every file with a comment saying it was auto-generated. Use `header` to say something else, with one comment written for every line of your text, or pass an empty string to leave it out:
```python
secsie.generate_config_file(config, 'app.secsie', header="Managed by the deploy script.\n\nDo not edit by hand.")
```
```ini
# Managed by the deploy script.
#
# Do not edit by hand.
```
`generate_config` only returns the config text, with no header at all. A header is always written as comments, so it can never change what the file is read back as.

### What the generator can and can't write (`secsie` mode)
Generating a config keeps your *data*, not the original file. Comments, blank lines, spacing, and spellings like `yes`/`no` (they come back as `true`/`false`) are not preserved, and `None` is written as `null`. A few things can't be represented in `secsie` at all, and the generator tells you instead of writing a config that reads back differently:
- Nested dicts raise a `TypeError`. `secsie` only has one level of sections.
- A string with a space followed by `#` raises a `ValueError`, since that would start an inline comment. A `#` with no space before it is fine.

Empty strings are commented out (see 2 above), so they are not present when the config is parsed again. Strings containing a comma are written as is, so they are read back as lists.

Keys that aren't in a section are always written before the first section, whatever order they have in your `dict`. A section only ends where the next one begins, so a key written after a section would be read back as part of it.

### Write an `.ini` file
Pass `mode='ini'` to write the same `ini` dialect that `mode='ini'` reads:
```python
config = {
    "app_name": "my app",
    "debug": False,
    "Database": {"host": "localhost", "port": 5432, "password": "p#ss", "zip": "02134", "tags": ["a", "b"], "motto": "work, play"},
}
print(secsie.generate_config(config, mode='ini'))
# or: secsie.generate_config_file(config, 'my_settings.ini', mode='ini')
```
```ini
app_name = my app
debug = false

[Database]
host = localhost
port = 5432
password = p#ss
zip = "02134"
tags = a, b
motto = "work, play"
```
There is no one `.ini` standard (PHP, Python's `configparser`, and Windows all disagree on the details), so the promise is a narrow one: **reading the result with `mode='ini'` gives you back exactly the data you wrote.** Nothing more is promised about other programs, though the output is plain `key = value` lines that most of them will read.

How it differs from `secsie` mode:
- Strings that would otherwise change are quoted: ones that look like a number, a boolean, or `null` (`"02134"`), ones with a comma (`"work, play"`), empty ones (`""`), and ones with leading or trailing whitespace. Everything else is written plain. Quoted values are kept as strings when read (see the `ini` differences above).
- Empty strings are written as `""` and read back, instead of being commented out.
- Section names can have spaces (`[CLI Server]`) and are never changed.
- Keys in a section are not indented unless you pass `indent`. As in `secsie` mode, top level keys are written first.
- `generate_config_file` starts the file with a `;` comment (including a custom `header`) instead of a `#` one, because `;` is the comment character `ini` readers agree on.
- Lists are written as `a, b`. A list with one item gets a trailing comma (`a,`) and an empty list is a lone comma (`,`), so they are still read as lists.

Whatever can't be written faithfully raises an error that names the key and says why, so the file is never written with a different value than you gave. Besides nested dicts, these can not be written in `ini` mode:
- strings with a space followed by `#`, a line break, or whitespace touching an `=` (the reader would change or cut them)
- strings that start or end with a quote character (there is no way to escape a quote)
- list items that are empty, have surrounding whitespace, contain a comma, or look like a number, boolean, or `null` (list items can't be quoted), or a list that starts with a quote
- `inf` and `nan`, keys with whitespace or `=` or that start with `#` or `;`, and section names with anything other than letters, numbers, spaces, `_`, and `-`

An empty section is written, but there is nothing in it for the reader to return, so it is not read back.
