hotchpotch 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,186 @@
1
+ # This file is autogenerated by maturin v1.12.6
2
+ # To update, run
3
+ #
4
+ # maturin generate-ci github
5
+ #
6
+ name: CI
7
+
8
+ on:
9
+ push:
10
+ branches:
11
+ - main
12
+ - master
13
+ tags:
14
+ - '*'
15
+ pull_request:
16
+ workflow_dispatch:
17
+
18
+ permissions:
19
+ contents: read
20
+
21
+ jobs:
22
+ linux:
23
+ runs-on: ${{ matrix.platform.runner }}
24
+ strategy:
25
+ matrix:
26
+ platform:
27
+ - runner: ubuntu-22.04
28
+ target: x86_64
29
+ - runner: ubuntu-22.04
30
+ target: x86
31
+ - runner: ubuntu-22.04
32
+ target: aarch64
33
+ - runner: ubuntu-22.04
34
+ target: armv7
35
+ - runner: ubuntu-22.04
36
+ target: s390x
37
+ - runner: ubuntu-22.04
38
+ target: ppc64le
39
+ steps:
40
+ - uses: actions/checkout@v6
41
+ - uses: actions/setup-python@v6
42
+ with:
43
+ python-version: 3.x
44
+ - name: Build wheels
45
+ uses: PyO3/maturin-action@v1
46
+ with:
47
+ target: ${{ matrix.platform.target }}
48
+ args: --release --out dist --find-interpreter
49
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
50
+ manylinux: auto
51
+ - name: Upload wheels
52
+ uses: actions/upload-artifact@v6
53
+ with:
54
+ name: wheels-linux-${{ matrix.platform.target }}
55
+ path: dist
56
+
57
+ musllinux:
58
+ runs-on: ${{ matrix.platform.runner }}
59
+ strategy:
60
+ matrix:
61
+ platform:
62
+ - runner: ubuntu-22.04
63
+ target: x86_64
64
+ - runner: ubuntu-22.04
65
+ target: x86
66
+ - runner: ubuntu-22.04
67
+ target: aarch64
68
+ - runner: ubuntu-22.04
69
+ target: armv7
70
+ steps:
71
+ - uses: actions/checkout@v6
72
+ - uses: actions/setup-python@v6
73
+ with:
74
+ python-version: 3.x
75
+ - name: Build wheels
76
+ uses: PyO3/maturin-action@v1
77
+ with:
78
+ target: ${{ matrix.platform.target }}
79
+ args: --release --out dist --find-interpreter
80
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
81
+ manylinux: musllinux_1_2
82
+ - name: Upload wheels
83
+ uses: actions/upload-artifact@v6
84
+ with:
85
+ name: wheels-musllinux-${{ matrix.platform.target }}
86
+ path: dist
87
+
88
+ windows:
89
+ runs-on: ${{ matrix.platform.runner }}
90
+ strategy:
91
+ matrix:
92
+ platform:
93
+ - runner: windows-latest
94
+ target: x64
95
+ python_arch: x64
96
+ - runner: windows-latest
97
+ target: x86
98
+ python_arch: x86
99
+ - runner: windows-11-arm
100
+ target: aarch64
101
+ python_arch: arm64
102
+ steps:
103
+ - uses: actions/checkout@v6
104
+ - uses: actions/setup-python@v6
105
+ with:
106
+ python-version: 3.13
107
+ architecture: ${{ matrix.platform.python_arch }}
108
+ - name: Build wheels
109
+ uses: PyO3/maturin-action@v1
110
+ with:
111
+ target: ${{ matrix.platform.target }}
112
+ args: --release --out dist --find-interpreter
113
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
114
+ - name: Upload wheels
115
+ uses: actions/upload-artifact@v6
116
+ with:
117
+ name: wheels-windows-${{ matrix.platform.target }}
118
+ path: dist
119
+
120
+ macos:
121
+ runs-on: ${{ matrix.platform.runner }}
122
+ strategy:
123
+ matrix:
124
+ platform:
125
+ - runner: macos-15-intel
126
+ target: x86_64
127
+ - runner: macos-latest
128
+ target: aarch64
129
+ steps:
130
+ - uses: actions/checkout@v6
131
+ - uses: actions/setup-python@v6
132
+ with:
133
+ python-version: 3.x
134
+ - name: Build wheels
135
+ uses: PyO3/maturin-action@v1
136
+ with:
137
+ target: ${{ matrix.platform.target }}
138
+ args: --release --out dist --find-interpreter
139
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
140
+ - name: Upload wheels
141
+ uses: actions/upload-artifact@v6
142
+ with:
143
+ name: wheels-macos-${{ matrix.platform.target }}
144
+ path: dist
145
+
146
+ sdist:
147
+ runs-on: ubuntu-latest
148
+ steps:
149
+ - uses: actions/checkout@v6
150
+ - name: Build sdist
151
+ uses: PyO3/maturin-action@v1
152
+ with:
153
+ command: sdist
154
+ args: --out dist
155
+ - name: Upload sdist
156
+ uses: actions/upload-artifact@v6
157
+ with:
158
+ name: wheels-sdist
159
+ path: dist
160
+
161
+ release:
162
+ name: Release
163
+ runs-on: ubuntu-latest
164
+ if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}
165
+ needs: [linux, musllinux, windows, macos, sdist]
166
+ permissions:
167
+ # Use to sign the release artifacts
168
+ id-token: write
169
+ # Used to upload release artifacts
170
+ contents: write
171
+ # Used to generate artifact attestation
172
+ attestations: write
173
+ steps:
174
+ - uses: actions/download-artifact@v7
175
+ - name: Generate artifact attestation
176
+ uses: actions/attest-build-provenance@v3
177
+ with:
178
+ subject-path: 'wheels-*/*'
179
+ - name: Install uv
180
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
181
+ uses: astral-sh/setup-uv@v7
182
+ - name: Publish to PyPI
183
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
184
+ run: uv publish 'wheels-*/*'
185
+ env:
186
+ UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,72 @@
1
+ /target
2
+
3
+ # Byte-compiled / optimized / DLL files
4
+ __pycache__/
5
+ .pytest_cache/
6
+ *.py[cod]
7
+
8
+ # C extensions
9
+ *.so
10
+
11
+ # Distribution / packaging
12
+ .Python
13
+ .venv/
14
+ env/
15
+ bin/
16
+ build/
17
+ develop-eggs/
18
+ dist/
19
+ eggs/
20
+ lib/
21
+ lib64/
22
+ parts/
23
+ sdist/
24
+ var/
25
+ include/
26
+ man/
27
+ venv/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+
32
+ # Installer logs
33
+ pip-log.txt
34
+ pip-delete-this-directory.txt
35
+ pip-selfcheck.json
36
+
37
+ # Unit test / coverage reports
38
+ htmlcov/
39
+ .tox/
40
+ .coverage
41
+ .cache
42
+ nosetests.xml
43
+ coverage.xml
44
+
45
+ # Translations
46
+ *.mo
47
+
48
+ # Mr Developer
49
+ .mr.developer.cfg
50
+ .project
51
+ .pydevproject
52
+
53
+ # Rope
54
+ .ropeproject
55
+
56
+ # Django stuff:
57
+ *.log
58
+ *.pot
59
+
60
+ .DS_Store
61
+
62
+ # Sphinx documentation
63
+ docs/_build/
64
+
65
+ # PyCharm
66
+ .idea/
67
+
68
+ # VSCode
69
+ .vscode/
70
+
71
+ # Pyenv
72
+ .python-version
@@ -0,0 +1,172 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "autocfg"
7
+ version = "1.5.0"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
10
+
11
+ [[package]]
12
+ name = "heck"
13
+ version = "0.5.0"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
16
+
17
+ [[package]]
18
+ name = "hotchpotch"
19
+ version = "0.1.0"
20
+ dependencies = [
21
+ "pyo3",
22
+ ]
23
+
24
+ [[package]]
25
+ name = "indoc"
26
+ version = "2.0.7"
27
+ source = "registry+https://github.com/rust-lang/crates.io-index"
28
+ checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
29
+ dependencies = [
30
+ "rustversion",
31
+ ]
32
+
33
+ [[package]]
34
+ name = "libc"
35
+ version = "0.2.182"
36
+ source = "registry+https://github.com/rust-lang/crates.io-index"
37
+ checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
38
+
39
+ [[package]]
40
+ name = "memoffset"
41
+ version = "0.9.1"
42
+ source = "registry+https://github.com/rust-lang/crates.io-index"
43
+ checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
44
+ dependencies = [
45
+ "autocfg",
46
+ ]
47
+
48
+ [[package]]
49
+ name = "once_cell"
50
+ version = "1.21.3"
51
+ source = "registry+https://github.com/rust-lang/crates.io-index"
52
+ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
53
+
54
+ [[package]]
55
+ name = "portable-atomic"
56
+ version = "1.13.1"
57
+ source = "registry+https://github.com/rust-lang/crates.io-index"
58
+ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
59
+
60
+ [[package]]
61
+ name = "proc-macro2"
62
+ version = "1.0.106"
63
+ source = "registry+https://github.com/rust-lang/crates.io-index"
64
+ checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
65
+ dependencies = [
66
+ "unicode-ident",
67
+ ]
68
+
69
+ [[package]]
70
+ name = "pyo3"
71
+ version = "0.27.2"
72
+ source = "registry+https://github.com/rust-lang/crates.io-index"
73
+ checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d"
74
+ dependencies = [
75
+ "indoc",
76
+ "libc",
77
+ "memoffset",
78
+ "once_cell",
79
+ "portable-atomic",
80
+ "pyo3-build-config",
81
+ "pyo3-ffi",
82
+ "pyo3-macros",
83
+ "unindent",
84
+ ]
85
+
86
+ [[package]]
87
+ name = "pyo3-build-config"
88
+ version = "0.27.2"
89
+ source = "registry+https://github.com/rust-lang/crates.io-index"
90
+ checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6"
91
+ dependencies = [
92
+ "target-lexicon",
93
+ ]
94
+
95
+ [[package]]
96
+ name = "pyo3-ffi"
97
+ version = "0.27.2"
98
+ source = "registry+https://github.com/rust-lang/crates.io-index"
99
+ checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089"
100
+ dependencies = [
101
+ "libc",
102
+ "pyo3-build-config",
103
+ ]
104
+
105
+ [[package]]
106
+ name = "pyo3-macros"
107
+ version = "0.27.2"
108
+ source = "registry+https://github.com/rust-lang/crates.io-index"
109
+ checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02"
110
+ dependencies = [
111
+ "proc-macro2",
112
+ "pyo3-macros-backend",
113
+ "quote",
114
+ "syn",
115
+ ]
116
+
117
+ [[package]]
118
+ name = "pyo3-macros-backend"
119
+ version = "0.27.2"
120
+ source = "registry+https://github.com/rust-lang/crates.io-index"
121
+ checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9"
122
+ dependencies = [
123
+ "heck",
124
+ "proc-macro2",
125
+ "pyo3-build-config",
126
+ "quote",
127
+ "syn",
128
+ ]
129
+
130
+ [[package]]
131
+ name = "quote"
132
+ version = "1.0.44"
133
+ source = "registry+https://github.com/rust-lang/crates.io-index"
134
+ checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
135
+ dependencies = [
136
+ "proc-macro2",
137
+ ]
138
+
139
+ [[package]]
140
+ name = "rustversion"
141
+ version = "1.0.22"
142
+ source = "registry+https://github.com/rust-lang/crates.io-index"
143
+ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
144
+
145
+ [[package]]
146
+ name = "syn"
147
+ version = "2.0.117"
148
+ source = "registry+https://github.com/rust-lang/crates.io-index"
149
+ checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
150
+ dependencies = [
151
+ "proc-macro2",
152
+ "quote",
153
+ "unicode-ident",
154
+ ]
155
+
156
+ [[package]]
157
+ name = "target-lexicon"
158
+ version = "0.13.5"
159
+ source = "registry+https://github.com/rust-lang/crates.io-index"
160
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
161
+
162
+ [[package]]
163
+ name = "unicode-ident"
164
+ version = "1.0.24"
165
+ source = "registry+https://github.com/rust-lang/crates.io-index"
166
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
167
+
168
+ [[package]]
169
+ name = "unindent"
170
+ version = "0.2.4"
171
+ source = "registry+https://github.com/rust-lang/crates.io-index"
172
+ checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
@@ -0,0 +1,13 @@
1
+ [package]
2
+ name = "hotchpotch"
3
+ version = "0.1.0"
4
+ edition = "2024"
5
+ readme = "README.md"
6
+
7
+ # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8
+ [lib]
9
+ name = "hotchpotch"
10
+ crate-type = ["cdylib"]
11
+
12
+ [dependencies]
13
+ pyo3 = { version = "0.27.0", features = ["extension-module", "abi3-py39"]}
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: hotchpotch
3
+ Version: 0.1.0
4
+ Classifier: Development Status :: 3 - Alpha
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
7
+ Classifier: Topic :: Text Processing :: General
8
+ Classifier: Programming Language :: Rust
9
+ Classifier: Programming Language :: Python :: Implementation :: CPython
10
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
11
+ Summary: Fast Rust-powered serialization format for embedding Python objects in CSV fields
12
+ Keywords: csv,serialization,parser,rust,pyo3
13
+ License-Expression: MIT
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
16
+ Project-URL: Homepage, https://github.com/gebz97/hotchpotch
17
+ Project-URL: Issues, https://github.com/gebz97/hotchpotch/issues
18
+ Project-URL: Repository, https://github.com/gebz97/hotchpotch
19
+
20
+ # hotchpotch
21
+
22
+ Fast Rust-powered serialization for embedding Python objects in CSV fields.
23
+
24
+ ## Format
25
+ ```
26
+ name=adam;hobbies=[cycling|rowing|chess];height=175cm;
27
+ ```
28
+
29
+ Supports: strings, ints, floats, bools, None, lists, nested dicts.
30
+ Special characters are backslash-escaped. All delimiters are configurable.
31
+
32
+ ## Install
33
+ ```bash
34
+ pip install hotchpotch
35
+ ```
36
+
37
+ ## Usage
38
+ ```python
39
+ import hotchpotch
40
+
41
+ cfg = hotchpotch.FormatConfig()
42
+
43
+ s = hotchpotch.dumps({"name": "adam", "hobbies": ["cycling", "rowing"], "age": 30}, cfg)
44
+ # → "age=30;hobbies=[cycling|rowing];name=adam;"
45
+
46
+ data = hotchpotch.loads(s, cfg)
47
+ # → {"age": 30, "hobbies": ["cycling", "rowing"], "name": "adam"}
48
+ ```
49
+
50
+ ## Custom delimiters
51
+ ```python
52
+ cfg = hotchpotch.FormatConfig(field_sep='&', kv_sep=':', list_sep=',')
53
+ ```
@@ -0,0 +1,34 @@
1
+ # hotchpotch
2
+
3
+ Fast Rust-powered serialization for embedding Python objects in CSV fields.
4
+
5
+ ## Format
6
+ ```
7
+ name=adam;hobbies=[cycling|rowing|chess];height=175cm;
8
+ ```
9
+
10
+ Supports: strings, ints, floats, bools, None, lists, nested dicts.
11
+ Special characters are backslash-escaped. All delimiters are configurable.
12
+
13
+ ## Install
14
+ ```bash
15
+ pip install hotchpotch
16
+ ```
17
+
18
+ ## Usage
19
+ ```python
20
+ import hotchpotch
21
+
22
+ cfg = hotchpotch.FormatConfig()
23
+
24
+ s = hotchpotch.dumps({"name": "adam", "hobbies": ["cycling", "rowing"], "age": 30}, cfg)
25
+ # → "age=30;hobbies=[cycling|rowing];name=adam;"
26
+
27
+ data = hotchpotch.loads(s, cfg)
28
+ # → {"age": 30, "hobbies": ["cycling", "rowing"], "name": "adam"}
29
+ ```
30
+
31
+ ## Custom delimiters
32
+ ```python
33
+ cfg = hotchpotch.FormatConfig(field_sep='&', kv_sep=':', list_sep=',')
34
+ ```
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["maturin>=1.12,<2.0"]
3
+ build-backend = "maturin"
4
+
5
+ [project]
6
+ name = "hotchpotch"
7
+ requires-python = ">=3.8"
8
+ keywords = ["csv", "serialization", "parser", "rust", "pyo3"]
9
+
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Intended Audience :: Developers",
13
+ "Topic :: Software Development :: Libraries :: Python Modules",
14
+ "Topic :: Text Processing :: General",
15
+ "Programming Language :: Rust",
16
+ "Programming Language :: Python :: Implementation :: CPython",
17
+ "Programming Language :: Python :: Implementation :: PyPy",
18
+ ]
19
+ dynamic = ["version"]
20
+ description = "Fast Rust-powered serialization format for embedding Python objects in CSV fields"
21
+ readme = "README.md"
22
+ license = "MIT"
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/gebz97/hotchpotch"
26
+ Repository = "https://github.com/gebz97/hotchpotch"
27
+ Issues = "https://github.com/gebz97/hotchpotch/issues"
@@ -0,0 +1,127 @@
1
+ use pyo3::prelude::*;
2
+
3
+ /*
4
+ All delimiters that define the serialization format.
5
+ Defaults match the example: name=adam;hobbies=[cycling|rowing|chess];height=175cm;
6
+ */
7
+ #[pyclass]
8
+ #[derive(Clone, Debug)]
9
+ pub struct FormatConfig {
10
+ // between key=value pairs → ';'
11
+ #[pyo3(get, set)]
12
+ pub field_sep: char,
13
+ // between key and value → '='
14
+ #[pyo3(get, set)]
15
+ pub kv_sep: char,
16
+ // list open bracket → '['
17
+ #[pyo3(get, set)]
18
+ pub list_open: char,
19
+ // list close bracket → ']'
20
+ #[pyo3(get, set)]
21
+ pub list_close: char,
22
+ // between list items → '|'
23
+ #[pyo3(get, set)]
24
+ pub list_sep: char,
25
+ // nested object open → '{'
26
+ #[pyo3(get, set)]
27
+ pub obj_open: char,
28
+ // nested object close → '}'
29
+ #[pyo3(get, set)]
30
+ pub obj_close: char,
31
+ // escape character → '\'
32
+ #[pyo3(get, set)]
33
+ pub escape: char,
34
+ // null representation → 'null'
35
+ #[pyo3(get, set)]
36
+ pub null_str: String,
37
+ // bool true representation → 'true'
38
+ #[pyo3(get, set)]
39
+ pub true_str: String,
40
+ // bool false representation→ 'false'
41
+ #[pyo3(get, set)]
42
+ pub false_str: String,
43
+ }
44
+
45
+ #[pymethods]
46
+ impl FormatConfig {
47
+ #[new]
48
+ #[pyo3(signature = (
49
+ field_sep = ';',
50
+ kv_sep = '=',
51
+ list_open = '[',
52
+ list_close = ']',
53
+ list_sep = '|',
54
+ obj_open = '{',
55
+ obj_close = '}',
56
+ escape = '\\',
57
+ null_str = "null".to_string(),
58
+ true_str = "true".to_string(),
59
+ false_str = "false".to_string(),
60
+ ))]
61
+ pub fn new(
62
+ field_sep: char,
63
+ kv_sep: char,
64
+ list_open: char,
65
+ list_close: char,
66
+ list_sep: char,
67
+ obj_open: char,
68
+ obj_close: char,
69
+ escape: char,
70
+ null_str: String,
71
+ true_str: String,
72
+ false_str: String,
73
+ ) -> Self {
74
+ FormatConfig {
75
+ field_sep,
76
+ kv_sep,
77
+ list_open,
78
+ list_close,
79
+ list_sep,
80
+ obj_open,
81
+ obj_close,
82
+ escape,
83
+ null_str,
84
+ true_str,
85
+ false_str,
86
+ }
87
+ }
88
+
89
+ fn __repr__(&self) -> String {
90
+ format!(
91
+ "FormatConfig(field_sep={:?}, kv_sep={:?}, list_sep={:?})",
92
+ self.field_sep, self.kv_sep, self.list_sep
93
+ )
94
+ }
95
+
96
+ /// Returns the set of all characters that must be escaped in scalar values
97
+ pub fn special_chars(&self) -> Vec<char> {
98
+ vec![
99
+ self.field_sep,
100
+ self.kv_sep,
101
+ self.list_open,
102
+ self.list_close,
103
+ self.list_sep,
104
+ self.obj_open,
105
+ self.obj_close,
106
+ self.escape,
107
+ ]
108
+ }
109
+ }
110
+
111
+ impl Default for FormatConfig {
112
+ fn default() -> Self {
113
+ FormatConfig::new(
114
+ ';',
115
+ '=',
116
+ '[',
117
+ ']',
118
+ '|',
119
+ '{',
120
+ '}',
121
+ '\\',
122
+ "null".into(),
123
+ "true".into(),
124
+ "false".into(),
125
+ )
126
+ }
127
+ }
@@ -0,0 +1,127 @@
1
+ mod config;
2
+ mod parser;
3
+ mod serializer;
4
+ mod value;
5
+
6
+ use pyo3::exceptions::PyValueError;
7
+ use pyo3::prelude::*;
8
+ use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyList, PyString};
9
+
10
+ use config::FormatConfig;
11
+ use value::Value;
12
+
13
+ // ── Python → internal Value ───────────────────────────────────────────────────
14
+
15
+ fn py_to_value(obj: &Bound<'_, PyAny>) -> PyResult<Value> {
16
+ // Order matters: Bool must come before Int (bool is subclass of int in Python)
17
+ if obj.is_none() {
18
+ Ok(Value::Null)
19
+ } else if obj.is_instance_of::<PyBool>() {
20
+ Ok(Value::Bool(obj.extract::<bool>()?))
21
+ } else if obj.is_instance_of::<PyInt>() {
22
+ Ok(Value::Int(obj.extract::<i64>()?))
23
+ } else if obj.is_instance_of::<PyFloat>() {
24
+ Ok(Value::Float(obj.extract::<f64>()?))
25
+ } else if obj.is_instance_of::<PyString>() {
26
+ Ok(Value::Str(obj.extract::<String>()?))
27
+ } else if obj.is_instance_of::<PyList>() {
28
+ let list = obj.cast::<PyList>()?;
29
+ let items: PyResult<Vec<Value>> = list.iter().map(|x| py_to_value(&x)).collect();
30
+ Ok(Value::List(items?))
31
+ } else if obj.is_instance_of::<PyDict>() {
32
+ let dict = obj.cast::<PyDict>()?;
33
+ let mut map = std::collections::BTreeMap::new();
34
+ for (k, v) in dict.iter() {
35
+ let key: String = k.extract()?;
36
+ map.insert(key, py_to_value(&v)?);
37
+ }
38
+ Ok(Value::Object(map))
39
+ } else {
40
+ // Fallback: convert to string via __str__
41
+ Ok(Value::Str(obj.str()?.extract::<String>()?))
42
+ }
43
+ }
44
+
45
+ // ── internal Value → Python ───────────────────────────────────────────────────
46
+
47
+ fn value_to_py<'py>(val: &Value, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
48
+ match val {
49
+ Value::Null => Ok(py.None().into_bound(py)),
50
+
51
+ // .to_owned() converts Borrowed<'_, '_, T> → Bound<'py, T> before into_any()
52
+ Value::Bool(b) => Ok(b.into_pyobject(py)?.to_owned().into_any()),
53
+ Value::Int(i) => Ok(i.into_pyobject(py)?.to_owned().into_any()),
54
+ Value::Float(f) => Ok(f.into_pyobject(py)?.to_owned().into_any()),
55
+
56
+ // String already returns an owned Bound, no .to_owned() needed
57
+ Value::Str(s) => Ok(s.clone().into_pyobject(py)?.into_any()),
58
+
59
+ Value::List(items) => {
60
+ let list = PyList::empty(py);
61
+ for item in items {
62
+ list.append(value_to_py(item, py)?)?;
63
+ }
64
+ Ok(list.into_any())
65
+ }
66
+ Value::Object(map) => {
67
+ let dict = PyDict::new(py);
68
+ for (k, v) in map {
69
+ dict.set_item(k, value_to_py(v, py)?)?;
70
+ }
71
+ Ok(dict.into_any())
72
+ }
73
+ }
74
+ }
75
+
76
+ // ── Exposed Python functions ──────────────────────────────────────────────────
77
+
78
+ /// Serialize a Python dict into the custom format string.
79
+ ///
80
+ /// Example:
81
+ /// cfg = FormatConfig()
82
+ /// dumps({"name": "adam", "hobbies": ["cycling", "rowing"], "age": 30}, cfg)
83
+ /// # → "name=adam;hobbies=[cycling|rowing];age=30;"
84
+ #[pyfunction]
85
+ #[pyo3(signature = (obj, config=None))]
86
+ fn dumps(obj: &Bound<'_, PyDict>, config: Option<&FormatConfig>) -> PyResult<String> {
87
+ let cfg = config.cloned().unwrap_or_default();
88
+ let mut fields: Vec<(String, Value)> = Vec::new();
89
+
90
+ for (k, v) in obj.iter() {
91
+ let key: String = k.extract()?;
92
+ let val = py_to_value(&v)?;
93
+ fields.push((key, val));
94
+ }
95
+
96
+ Ok(serializer::serialize_object(&fields, &cfg))
97
+ }
98
+
99
+ /// Deserialize a custom format string into a Python dict.
100
+ ///
101
+ /// Example:
102
+ /// cfg = FormatConfig()
103
+ /// loads("name=adam;hobbies=[cycling|rowing];age=30;", cfg)
104
+ /// # → {"name": "adam", "hobbies": ["cycling", "rowing"], "age": 30}
105
+ #[pyfunction]
106
+ #[pyo3(signature = (s, config=None))]
107
+ fn loads(py: Python<'_>, s: &str, config: Option<&FormatConfig>) -> PyResult<Py<PyAny>> {
108
+ let cfg = config.cloned().unwrap_or_default();
109
+
110
+ let fields = parser::parse_record_str(s, &cfg).map_err(|e| PyValueError::new_err(e))?;
111
+
112
+ let dict = PyDict::new(py);
113
+ for (k, v) in &fields {
114
+ dict.set_item(k, value_to_py(v, py)?)?;
115
+ }
116
+ Ok(dict.into())
117
+ }
118
+
119
+ // ── Module ────────────────────────────────────────────────────────────────────
120
+
121
+ #[pymodule]
122
+ fn hotchpotch(m: &Bound<'_, PyModule>) -> PyResult<()> {
123
+ m.add_class::<FormatConfig>()?;
124
+ m.add_function(wrap_pyfunction!(dumps, m)?)?;
125
+ m.add_function(wrap_pyfunction!(loads, m)?)?;
126
+ Ok(())
127
+ }
@@ -0,0 +1,165 @@
1
+ use std::collections::BTreeMap;
2
+ use crate::config::FormatConfig;
3
+ use crate::value::Value;
4
+
5
+ pub struct Parser<'a> {
6
+ input: &'a [char],
7
+ pos: usize,
8
+ cfg: &'a FormatConfig,
9
+ }
10
+
11
+ impl<'a> Parser<'a> {
12
+ pub fn new(chars: &'a [char], cfg: &'a FormatConfig) -> Self {
13
+ Parser { input: chars, pos: 0, cfg }
14
+ }
15
+
16
+ fn peek(&self) -> Option<char> {
17
+ self.input.get(self.pos).copied()
18
+ }
19
+
20
+ fn advance(&mut self) -> Option<char> {
21
+ let ch = self.input.get(self.pos).copied();
22
+ self.pos += 1;
23
+ ch
24
+ }
25
+
26
+ fn expect(&mut self, ch: char) -> Result<(), String> {
27
+ match self.advance() {
28
+ Some(c) if c == ch => Ok(()),
29
+ Some(c) => Err(format!("Expected {:?} but got {:?} at pos {}", ch, c, self.pos - 1)),
30
+ None => Err(format!("Expected {:?} but got EOF", ch)),
31
+ }
32
+ }
33
+
34
+ /// Read a scalar string, handling escape sequences, stopping at any
35
+ /// unescaped special character in `stop_at`.
36
+ fn read_scalar(&mut self, stop_at: &[char]) -> String {
37
+ let mut out = String::new();
38
+ loop {
39
+ match self.peek() {
40
+ None => break,
41
+ Some(ch) if ch == self.cfg.escape => {
42
+ self.advance(); // consume escape char
43
+ if let Some(next) = self.advance() {
44
+ out.push(next); // take the literal next char
45
+ }
46
+ }
47
+ Some(ch) if stop_at.contains(&ch) => break,
48
+ Some(ch) => { self.advance(); out.push(ch); }
49
+ }
50
+ }
51
+ out
52
+ }
53
+
54
+ /// Parse a value: list, nested object, or scalar.
55
+ pub fn parse_value(&mut self) -> Result<Value, String> {
56
+ match self.peek() {
57
+ Some(ch) if ch == self.cfg.list_open => self.parse_list(),
58
+ Some(ch) if ch == self.cfg.obj_open => self.parse_nested_object(),
59
+ _ => self.parse_scalar(),
60
+ }
61
+ }
62
+
63
+ fn parse_list(&mut self) -> Result<Value, String> {
64
+ self.expect(self.cfg.list_open)?;
65
+ let mut items = Vec::new();
66
+
67
+ // Handle empty list []
68
+ if self.peek() == Some(self.cfg.list_close) {
69
+ self.advance();
70
+ return Ok(Value::List(items));
71
+ }
72
+
73
+ loop {
74
+ items.push(self.parse_value()?);
75
+ match self.peek() {
76
+ Some(ch) if ch == self.cfg.list_sep => { self.advance(); }
77
+ Some(ch) if ch == self.cfg.list_close => { self.advance(); break; }
78
+ Some(ch) => return Err(format!("Unexpected {:?} in list at pos {}", ch, self.pos)),
79
+ None => return Err("Unterminated list".into()),
80
+ }
81
+ }
82
+ Ok(Value::List(items))
83
+ }
84
+
85
+ fn parse_nested_object(&mut self) -> Result<Value, String> {
86
+ self.expect(self.cfg.obj_open)?;
87
+ let mut map = BTreeMap::new();
88
+
89
+ if self.peek() == Some(self.cfg.obj_close) {
90
+ self.advance();
91
+ return Ok(Value::Object(map));
92
+ }
93
+
94
+ loop {
95
+ // Stop chars for key: kv_sep
96
+ let key = self.read_scalar(&[self.cfg.kv_sep, self.cfg.obj_close]);
97
+ self.expect(self.cfg.kv_sep)?;
98
+
99
+ // Stop chars for value inside nested obj: field_sep or obj_close
100
+ let val = self.parse_value()?;
101
+ map.insert(key, val);
102
+
103
+ match self.peek() {
104
+ Some(ch) if ch == self.cfg.field_sep => { self.advance(); }
105
+ Some(ch) if ch == self.cfg.obj_close => { self.advance(); break; }
106
+ Some(ch) => return Err(format!("Unexpected {:?} in object at pos {}", ch, self.pos)),
107
+ None => return Err("Unterminated object".into()),
108
+ }
109
+ }
110
+ Ok(Value::Object(map))
111
+ }
112
+
113
+ fn parse_scalar(&mut self) -> Result<Value, String> {
114
+ // Stop at anything that could end a scalar in any context
115
+ let stop = vec![
116
+ self.cfg.field_sep, self.cfg.kv_sep,
117
+ self.cfg.list_sep, self.cfg.list_close,
118
+ self.cfg.obj_close,
119
+ ];
120
+ let s = self.read_scalar(&stop);
121
+ Ok(coerce_scalar(s, self.cfg))
122
+ }
123
+
124
+ /// Parse a full top-level record: key=val;key=val;
125
+ pub fn parse_record(&mut self) -> Result<Vec<(String, Value)>, String> {
126
+ let mut fields = Vec::new();
127
+
128
+ while self.pos < self.input.len() {
129
+ // Skip trailing field_sep at end of input
130
+ if self.peek() == Some(self.cfg.field_sep) {
131
+ self.advance();
132
+ continue;
133
+ }
134
+
135
+ let key = self.read_scalar(&[self.cfg.kv_sep]);
136
+ if key.is_empty() { break; }
137
+
138
+ self.expect(self.cfg.kv_sep)?;
139
+ let val = self.parse_value()?;
140
+ fields.push((key, val));
141
+
142
+ // consume trailing field_sep after value
143
+ if self.peek() == Some(self.cfg.field_sep) {
144
+ self.advance();
145
+ }
146
+ }
147
+ Ok(fields)
148
+ }
149
+ }
150
+
151
+ /// Infer the type of a scalar string (int → float → bool → null → str)
152
+ fn coerce_scalar(s: String, cfg: &FormatConfig) -> Value {
153
+ if s == cfg.null_str { return Value::Null; }
154
+ if s == cfg.true_str { return Value::Bool(true); }
155
+ if s == cfg.false_str { return Value::Bool(false); }
156
+ if let Ok(i) = s.parse::<i64>() { return Value::Int(i); }
157
+ if let Ok(f) = s.parse::<f64>() { return Value::Float(f); }
158
+ Value::Str(s)
159
+ }
160
+
161
+ pub fn parse_record_str(s: &str, cfg: &FormatConfig) -> Result<Vec<(String, Value)>, String> {
162
+ let chars: Vec<char> = s.chars().collect();
163
+ let mut parser = Parser::new(&chars, cfg);
164
+ parser.parse_record()
165
+ }
@@ -0,0 +1,70 @@
1
+ use crate::config::FormatConfig;
2
+ use crate::value::Value;
3
+
4
+ /// Escape a string scalar: prepend `config.escape` before any special char.
5
+ pub fn escape_str(s: &str, cfg: &FormatConfig) -> String {
6
+ let specials = cfg.special_chars();
7
+ let mut out = String::with_capacity(s.len());
8
+ for ch in s.chars() {
9
+ if specials.contains(&ch) {
10
+ out.push(cfg.escape);
11
+ }
12
+ out.push(ch);
13
+ }
14
+ out
15
+ }
16
+
17
+ /// Serialize a [`Value`] into a string using `cfg`.
18
+ pub fn serialize_value(val: &Value, cfg: &FormatConfig) -> String {
19
+ match val {
20
+ Value::Null => cfg.null_str.clone(),
21
+ Value::Bool(b) => if *b { cfg.true_str.clone() } else { cfg.false_str.clone() },
22
+ Value::Int(i) => i.to_string(),
23
+ Value::Float(f) => {
24
+ // Always include decimal point so floats are unambiguous from ints
25
+ if f.fract() == 0.0 { format!("{:.1}", f) } else { f.to_string() }
26
+ }
27
+ Value::Str(s) => escape_str(s, cfg),
28
+
29
+ Value::List(items) => {
30
+ let inner: Vec<String> = items.iter().map(|v| serialize_value(v, cfg)).collect();
31
+ format!(
32
+ "{}{}{}",
33
+ cfg.list_open,
34
+ inner.join(&cfg.list_sep.to_string()),
35
+ cfg.list_close,
36
+ )
37
+ }
38
+
39
+ Value::Object(map) => {
40
+ let inner: Vec<String> = map
41
+ .iter()
42
+ .map(|(k, v)| format!(
43
+ "{}{}{}",
44
+ escape_str(k, cfg),
45
+ cfg.kv_sep,
46
+ serialize_value(v, cfg)
47
+ ))
48
+ .collect();
49
+ format!(
50
+ "{}{}{}",
51
+ cfg.obj_open,
52
+ inner.join(&cfg.field_sep.to_string()),
53
+ cfg.obj_close,
54
+ )
55
+ }
56
+ }
57
+ }
58
+
59
+ /// Serialize a top-level object (dict of key→value) into the full format.
60
+ /// Produces: key1=val1;key2=val2;
61
+ pub fn serialize_object(map: &[(String, Value)], cfg: &FormatConfig) -> String {
62
+ let mut out = String::new();
63
+ for (k, v) in map {
64
+ out.push_str(&escape_str(k, cfg));
65
+ out.push(cfg.kv_sep);
66
+ out.push_str(&serialize_value(v, cfg));
67
+ out.push(cfg.field_sep);
68
+ }
69
+ out
70
+ }
@@ -0,0 +1,13 @@
1
+ use std::collections::BTreeMap; // BTreeMap keeps key order deterministic
2
+
3
+ /// Internal representation of any serializable value.
4
+ #[derive(Debug, Clone, PartialEq)]
5
+ pub enum Value {
6
+ Null,
7
+ Bool(bool),
8
+ Int(i64),
9
+ Float(f64),
10
+ Str(String),
11
+ List(Vec<Value>),
12
+ Object(BTreeMap<String, Value>), // preserves insertion order via BTree sort
13
+ }