bytesz 0.2.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.
Files changed (38) hide show
  1. bytesz-0.2.0/.gitattributes +1 -0
  2. bytesz-0.2.0/.gitignore +12 -0
  3. bytesz-0.2.0/CHANGELOG.md +57 -0
  4. bytesz-0.2.0/CONTRIBUTING.md +22 -0
  5. bytesz-0.2.0/GUIDE.md +141 -0
  6. bytesz-0.2.0/LICENSE +21 -0
  7. bytesz-0.2.0/PKG-INFO +147 -0
  8. bytesz-0.2.0/README.md +116 -0
  9. bytesz-0.2.0/pyproject.toml +61 -0
  10. bytesz-0.2.0/requirements-dev.txt +3 -0
  11. bytesz-0.2.0/scripts/check.ps1 +8 -0
  12. bytesz-0.2.0/scripts/e2e_wheel.py +89 -0
  13. bytesz-0.2.0/scripts/release.ps1 +28 -0
  14. bytesz-0.2.0/src/bytesz/__init__.py +44 -0
  15. bytesz-0.2.0/src/bytesz/__main__.py +10 -0
  16. bytesz-0.2.0/src/bytesz/_bits.py +81 -0
  17. bytesz-0.2.0/src/bytesz/_cli.py +163 -0
  18. bytesz-0.2.0/src/bytesz/_cli_support.py +74 -0
  19. bytesz-0.2.0/src/bytesz/_convert.py +50 -0
  20. bytesz-0.2.0/src/bytesz/_errors.py +42 -0
  21. bytesz-0.2.0/src/bytesz/_format.py +159 -0
  22. bytesz-0.2.0/src/bytesz/_hints.py +49 -0
  23. bytesz-0.2.0/src/bytesz/_parse.py +182 -0
  24. bytesz-0.2.0/src/bytesz/_types.py +70 -0
  25. bytesz-0.2.0/src/bytesz/_units.py +71 -0
  26. bytesz-0.2.0/src/bytesz/py.typed +0 -0
  27. bytesz-0.2.0/tests/test_bits_convert.py +87 -0
  28. bytesz-0.2.0/tests/test_cli.py +167 -0
  29. bytesz-0.2.0/tests/test_contract.py +42 -0
  30. bytesz-0.2.0/tests/test_docs.py +42 -0
  31. bytesz-0.2.0/tests/test_format.py +67 -0
  32. bytesz-0.2.0/tests/test_format_options.py +134 -0
  33. bytesz-0.2.0/tests/test_numfmt_oracle.py +115 -0
  34. bytesz-0.2.0/tests/test_parse.py +82 -0
  35. bytesz-0.2.0/tests/test_parse_options.py +159 -0
  36. bytesz-0.2.0/tests/test_promise.py +100 -0
  37. bytesz-0.2.0/tests/test_roundtrip.py +20 -0
  38. bytesz-0.2.0/tests/test_sources.py +66 -0
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ build/
6
+ dist/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
@@ -0,0 +1,57 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.0]
10
+
11
+ ### Added
12
+
13
+ - The 2022 SI prefixes ronna and quetta (`RB`, `QB`) and the IEC 80000-13:2025
14
+ prefixes robi and quebi (`RiB`, `QiB`).
15
+ - `format` options (also on `format_bits`): `system` may be omitted and then follows
16
+ `unit` (default IEC); `rounding` (`half-up`, `half-even`, `half-down`, `up`, `down`,
17
+ `ceiling`, `floor`), `unit` to force one unit, `signed`, `trailing_zeros`,
18
+ `decimal_point` and `separator`.
19
+ - `parse` options: `rounding` for values that are not whole bytes, `signed`,
20
+ `decimal_point`, and an opt-in `legacy` mode that reads `KB`, `MB`, `GB` (and bare
21
+ `K`, `M`, `G`) as powers of 1024. Bare IEC prefixes (`1.5Mi`) are accepted, and bare
22
+ SI prefixes (`1.5M`) when `system="si"` is pinned, matching `numfmt` output.
23
+ - `parse_bits` and `format_bits` for bit counts with the `bit` symbol.
24
+ - `convert(count, unit)` for the exact `Decimal` value in one unit.
25
+ - Typed errors `UnknownUnitError`, `SystemMismatchError` and `FractionalBytesError`,
26
+ all subclasses of `InvalidSizeError`; every error carries `.text` and `.unit`.
27
+ - `System` and `Rounding` type aliases, `IEC_SUFFIXES` and `SI_SUFFIXES`.
28
+ - A `bytesz` command with `format` and `parse` subcommands, stdin input and a
29
+ numfmt style `--field` option. Also runnable as `python -m bytesz`.
30
+ - GUIDE.md.
31
+
32
+ ### Changed
33
+
34
+ - Rounding now carries into the next unit: `format(1048575)` is `"1 MiB"` instead of
35
+ `"1024 KiB"`.
36
+ - Counts of 1024**9 bytes and above now use `RiB` and `QiB` (and `RB`, `QB` for SI)
37
+ instead of large numbers of `YiB`.
38
+ - A lower case `b` in `parse` now explains that it means bits, and a bit unit such
39
+ as `Kibit` raises `UnknownUnitError` pointing at `parse_bits`.
40
+
41
+ ### Fixed
42
+
43
+ - Parsing stays exact for long inputs and the largest units; the default decimal
44
+ context used to round products above 28 digits.
45
+ - Arithmetic runs in a private decimal context, so a caller's global settings
46
+ (traps, precision, exponent limits) no longer change or break the results.
47
+
48
+ ## [0.1.0]
49
+
50
+ - `parse` turns a human size into an exact integer number of bytes, choosing the
51
+ system from the unit: IEC suffixes (KiB, MiB) use 1024 and SI suffixes (kB, MB)
52
+ use 1000.
53
+ - `format` turns a byte count into a human size for either system with a chosen
54
+ precision.
55
+ - Strict by design: ambiguous spellings such as `KB` raise `AmbiguousUnitError`,
56
+ and inputs that do not land on a whole byte raise `InvalidSizeError`.
57
+ - `system` can pin `parse` to one system so the other is rejected.
@@ -0,0 +1,22 @@
1
+ # Contributing
2
+
3
+ Thanks for taking the time. This project keeps a small, predictable shape.
4
+
5
+ ## Setup
6
+
7
+ ```
8
+ pip install -r requirements-dev.txt
9
+ ```
10
+
11
+ ## Before you open a pull request
12
+
13
+ - Keep public API in `src/bytesz/__init__.py`; internals live in `_`-prefixed modules.
14
+ - Add a test for the behaviour you change. Tests inject time, disk and network; they
15
+ never touch the real thing.
16
+ - Run `scripts/check.ps1` (format, lint, types, tests, and an end to end run of the
17
+ built wheel through `scripts/e2e_wheel.py`) and make sure it passes. The GNU
18
+ `numfmt` cross-check runs when WSL with GNU coreutils is available and is skipped
19
+ otherwise.
20
+ - One logical change per commit, imperative subject line.
21
+
22
+ Open an issue first if the change is large or shifts the public API.
bytesz-0.2.0/GUIDE.md ADDED
@@ -0,0 +1,141 @@
1
+ # bytesz guide
2
+
3
+ This guide explains the rules `bytesz` follows and why. The short version: the unit
4
+ alone decides the meaning, arithmetic is exact, and nothing ambiguous is guessed.
5
+
6
+ ## Two systems
7
+
8
+ | Factor | IEC symbol | IEC name | SI symbol | SI name | SI factor |
9
+ | ------ | ---------- | -------- | --------- | ------- | --------- |
10
+ | 1024^1 | KiB | kibibyte | kB | kilobyte | 1000^1 |
11
+ | 1024^2 | MiB | mebibyte | MB | megabyte | 1000^2 |
12
+ | 1024^3 | GiB | gibibyte | GB | gigabyte | 1000^3 |
13
+ | 1024^4 | TiB | tebibyte | TB | terabyte | 1000^4 |
14
+ | 1024^5 | PiB | pebibyte | PB | petabyte | 1000^5 |
15
+ | 1024^6 | EiB | exbibyte | EB | exabyte | 1000^6 |
16
+ | 1024^7 | ZiB | zebibyte | ZB | zettabyte | 1000^7 |
17
+ | 1024^8 | YiB | yobibyte | YB | yottabyte | 1000^8 |
18
+ | 1024^9 | RiB | robibyte | RB | ronnabyte | 1000^9 |
19
+ | 1024^10 | QiB | quebibyte | QB | quettabyte | 1000^10 |
20
+
21
+ - The IEC binary prefixes were approved in December 1998 and published as an
22
+ amendment to IEC 60027-2; NIST documents them at
23
+ https://physics.nist.gov/cuu/Units/binary.html. NIST notes that they are not part
24
+ of the SI and that the symbol is `Ki`, not `ki`.
25
+ - Zebi and yobi were added in 2005. Ronna and quetta joined the SI in 2022
26
+ (https://www.bipm.org/en/measurement-units/si-prefixes), and their binary partners
27
+ robi and quebi were adopted in IEC 80000-13:2025.
28
+ - The SI kilo is a lower case `k`. `bytesz` writes `kB` and refuses `KB` by default.
29
+
30
+ ## Case rules
31
+
32
+ Units are case sensitive because case carries meaning: `B` is a byte, `b` is a bit,
33
+ `k` is kilo, `Ki` is kibi. `parse` accepts exactly the symbols above plus `B`, the
34
+ bare IEC prefixes (`1.5Mi`, as `numfmt --to=iec-i` writes them), and a bare number.
35
+ Anything that only matches when case is ignored (`KB`, `kib`, `Mb`, `GIB`) raises
36
+ `AmbiguousUnitError`; anything else raises `UnknownUnitError`. Whitespace between the
37
+ number and the unit is optional (`1.5GB` and `512 MiB` both work).
38
+
39
+ ## The legacy "KB = 1024" usage
40
+
41
+ Before 1999, and still today in places, `KB`, `MB` and `GB` meant powers of 1024.
42
+ JEDEC keeps those definitions for semiconductor memory, saying they are "included
43
+ only to reflect common usage", and several operating systems label sizes that way.
44
+ NIST also recalls the "1.44 MB" diskette, which holds 1 474 560 bytes: 1440 times
45
+ 1024, a third meaning of megabyte. So `KB` really is ambiguous, and `bytesz` will not
46
+ pick a side for you. If your input comes from such a source, say so:
47
+
48
+ ```python
49
+ from bytesz import parse
50
+
51
+ parse("4 GB", legacy=True) # 4294967296
52
+ parse("4G", legacy=True) # 4294967296, bare prefixes as numfmt --from=iec reads them
53
+ parse("4 kB", legacy=True) # 4000, lower case k is never legacy
54
+ ```
55
+
56
+ `legacy=True` cannot be combined with `system="si"`. Pinning `system="si"` on its
57
+ own makes bare prefixes (`1.5M`, `2K`) mean powers of 1000, as `numfmt --from=si` does.
58
+
59
+ ## Bits
60
+
61
+ `parse` rejects `Mb` because a bit rate mistaken for a byte size is off by a factor
62
+ of eight. Use `parse_bits` and `format_bits` for bit counts. They write the
63
+ unambiguous `bit` symbol (`kbit`, `Mibit`) and also read the short `b` form (`kb`,
64
+ `Mb`, `Kib`). Upper case `Kb` stays ambiguous there too.
65
+
66
+ ## Rounding and carry
67
+
68
+ `format` picks the largest unit that keeps the number at one or more, rounds to
69
+ `precision` decimals (default 2) and drops trailing zeros unless
70
+ `trailing_zeros=True`. If rounding pushes the number up to the base, it carries:
71
+ 1048575 bytes is `1 MiB`, not `1024 KiB`. Note what that means for IEC: 999.95 KiB
72
+ rounds to `1000 KiB` at one decimal, and that is correct, because 1000 KiB is still
73
+ below 1024 KiB. In SI, 999.95 kB does carry to `1 MB`. GNU `numfmt` behaves the same.
74
+
75
+ | bytesz rounding | ties | numfmt `--round` |
76
+ | --------------- | ---- | ---------------- |
77
+ | `half-up` (default) | away from zero | `nearest` |
78
+ | `half-even` | to the even digit | none |
79
+ | `half-down` | toward zero | none |
80
+ | `up` | away from zero always | `from-zero` |
81
+ | `down` | toward zero always | `towards-zero` |
82
+ | `ceiling` | toward positive infinity | `up` |
83
+ | `floor` | toward negative infinity | `down` |
84
+
85
+ Note that `numfmt` defaults to `from-zero` while `bytesz` defaults to `half-up`.
86
+ Pass `unit="MiB"` to force a unit; there is no carry then, and the unit picks the
87
+ system unless you also pass `system` (a unit from the other one raises
88
+ `SystemMismatchError`). `convert(count, unit)`
89
+ gives the exact `Decimal` value with no rounding at all.
90
+
91
+ ## Fractions when parsing
92
+
93
+ `parse("1.5 B")` raises `FractionalBytesError`: half a byte is a bug in the input.
94
+ Pass a rounding mode when you would rather round: `parse("0.1 KiB",
95
+ rounding="half-up")` is 102.
96
+
97
+ ## Exactness and huge values
98
+
99
+ All arithmetic is integer or `Decimal` with enough precision for the input, so
100
+ `parse("123456789012345678901234567890 QiB")` is exact and
101
+ `parse(format(n, precision=110)) == n` holds for any `n`. Every power of 1000 or 1024
102
+ divides into a terminating decimal, so `convert` never rounds.
103
+
104
+ ## Signs and separators
105
+
106
+ Negative values are refused unless `signed=True`, which also allows a leading `+`
107
+ when parsing. `decimal_point=","` reads and writes a comma; it is never inferred
108
+ from the locale. `separator` sets the text between number and unit when formatting.
109
+
110
+ ## Errors
111
+
112
+ | Error | Raised when | Subclass of |
113
+ | ----- | ----------- | ----------- |
114
+ | `ByteszError` | base class | `ValueError` |
115
+ | `InvalidSizeError` | malformed input, a sign without `signed=True` | `ByteszError` |
116
+ | `AmbiguousUnitError` | `KB`, `Mb`, `kib`, a bare `M` unless `system="si"` or `legacy=True` | `ByteszError` |
117
+ | `UnknownUnitError` | not a size unit, or a bit unit given to `parse` (and the reverse) | `InvalidSizeError` |
118
+ | `SystemMismatchError` | an SI unit with `system="iec"` or the reverse | `InvalidSizeError` |
119
+ | `FractionalBytesError` | not a whole byte and no `rounding` | `InvalidSizeError` |
120
+
121
+ Each carries `.text` (the input) and `.unit` (the unit involved) when known. Bad
122
+ option values (an unknown `system` or `rounding`) raise a plain `ValueError`, and a
123
+ non-string input to `parse` raises `TypeError`.
124
+
125
+ ## Command line
126
+
127
+ ```
128
+ bytesz format [--system iec|si] [--precision N] [--rounding MODE] [--unit U]
129
+ [--signed] [--trailing-zeros] [--separator S] [--decimal-point ,]
130
+ [--bits] [--field N] [VALUES ...]
131
+ bytesz parse [--system iec|si] [--legacy] [--rounding MODE] [--signed]
132
+ [--decimal-point ,] [--bits] [--field N] [VALUES ...]
133
+ ```
134
+
135
+ With no values, each line of stdin is one value. `--field N` converts only the Nth
136
+ whitespace separated field and keeps the rest of the line, so a size in that field
137
+ must be written without an inner space (`1.5MiB`). Blank lines pass through and a
138
+ leading byte order mark is ignored. `format` takes plain ASCII integers only. The
139
+ first error stops the run, goes to stderr, and the exit status is 1; usage errors,
140
+ such as a negative `--precision` or an unknown `--unit`, exit with 2. Messages name
141
+ the flags (`--signed`, `--legacy`) rather than the keyword arguments.
bytesz-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Javier Lianes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
bytesz-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.5
2
+ Name: bytesz
3
+ Version: 0.2.0
4
+ Summary: Strict two-way conversion between byte counts and human sizes with explicit IEC and SI units
5
+ Project-URL: Homepage, https://lianes8studio.com
6
+ Project-URL: Repository, https://github.com/JaviLianes8/bytesz
7
+ Project-URL: Issues, https://github.com/JaviLianes8/bytesz/issues
8
+ Project-URL: Changelog, https://github.com/JaviLianes8/bytesz/blob/main/CHANGELOG.md
9
+ Project-URL: LinkedIn, https://www.linkedin.com/in/jlianes/
10
+ Project-URL: Funding, https://buymeacoffee.com/jlianesglrs
11
+ Author-email: Javier Lianes <javier@lianes8studio.com>
12
+ License-Expression: MIT
13
+ License-File: LICENSE
14
+ Keywords: bytes,format,human,iec,kib,mib,numfmt,parse,si,size
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Environment :: Console
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Topic :: Utilities
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Provides-Extra: dev
25
+ Requires-Dist: build; extra == 'dev'
26
+ Requires-Dist: mypy; extra == 'dev'
27
+ Requires-Dist: pytest; extra == 'dev'
28
+ Requires-Dist: ruff; extra == 'dev'
29
+ Requires-Dist: twine; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # bytesz
33
+
34
+ Strict two-way conversion between byte counts and human sizes with explicit IEC and SI units.
35
+ Typed, exact (integer and `Decimal` arithmetic only), zero dependencies, with a small CLI.
36
+
37
+ ## Install
38
+
39
+ ```
40
+ pip install bytesz
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ```python
46
+ from bytesz import format, parse
47
+
48
+ # Human size to an exact integer number of bytes. The unit picks the system.
49
+ parse("1.5 MiB") # 1572864 (IEC, 1024 based)
50
+ parse("1.5 MB") # 1500000 (SI, 1000 based)
51
+ parse("512 B") # 512
52
+ parse("2048") # 2048 (a bare number is bytes)
53
+
54
+ # Bytes back to a human size, for whichever system you ask for.
55
+ format(1572864) # "1.5 MiB" (IEC is the default)
56
+ format(1500000, system="si") # "1.5 MB"
57
+ format(1234567, system="si", precision=2) # "1.23 MB"
58
+ format(1048575) # "1 MiB" (rounding carries into the next unit)
59
+ ```
60
+
61
+ It refuses to guess:
62
+
63
+ ```python
64
+ from bytesz import AmbiguousUnitError, FractionalBytesError, SystemMismatchError, parse
65
+
66
+ parse("1 KB") # AmbiguousUnitError: KB could mean kB (1000) or KiB (1024)
67
+ parse("100 Mb") # AmbiguousUnitError: a lower case b means bits, see parse_bits
68
+ parse("1.5 B") # FractionalBytesError: not a whole number of bytes
69
+ parse("1 kB", system="iec") # SystemMismatchError: an SI unit under a pinned IEC system
70
+ ```
71
+
72
+ Every input error is a `ByteszError` (a `ValueError`) carrying `.text` and `.unit`. Bad
73
+ option values raise a plain `ValueError`, and a non-string input to `parse` a `TypeError`.
74
+
75
+ ## Options
76
+
77
+ ```python
78
+ from bytesz import convert, format, format_bits, parse, parse_bits
79
+
80
+ # Formatting
81
+ format(1250, system="si", precision=1, rounding="half-even") # "1.2 kB"
82
+ format(1_000_000, unit="MiB") # "0.95 MiB"
83
+ format(5000, unit="kB") # "5 kB" (the unit picks the system)
84
+ format(-1536, signed=True) # "-1.5 KiB"
85
+ format(1536, precision=3, trailing_zeros=True) # "1.500 KiB"
86
+ format(1536, decimal_point=",", separator="") # "1,5KiB"
87
+
88
+ # Parsing
89
+ parse("1.0005 kB", rounding="half-up") # 1001
90
+ parse("1,5 MiB", decimal_point=",") # 1572864
91
+ parse("-2 KiB", signed=True) # -2048
92
+ parse("1.5M", system="si") # 1500000 (bare prefixes, as numfmt writes them)
93
+ parse("1.5Mi") # 1572864 (bare IEC prefix)
94
+ parse("4 GB", legacy=True) # 4294967296 (JEDEC style KB/MB/GB = 1024, opt-in)
95
+
96
+ # Bits, with the unambiguous "bit" symbol
97
+ parse_bits("100 Mbit") # 100000000
98
+ format_bits(1_500_000, system="si") # "1.5 Mbit"
99
+
100
+ # Exact value in one unit, no rounding at all
101
+ convert(1, "KiB") # Decimal("0.0009765625")
102
+ ```
103
+
104
+ Rounding modes: `half-up` (default, ties away from zero), `half-even`, `half-down`,
105
+ `up`, `down`, `ceiling`, `floor`. Units reach the 2022 SI prefixes `RB` and `QB` and
106
+ the IEC 80000-13:2025 prefixes `RiB` and `QiB`.
107
+
108
+ ## Command line
109
+
110
+ ```
111
+ bytesz format 1572864 # 1.5 MiB
112
+ bytesz format --system si 1234567 # 1.23 MB
113
+ bytesz parse "1.5 MiB" 2GB # 1572864, 2000000000
114
+ bytesz parse --legacy 4KB # 4096
115
+ du -b * | bytesz format --field 1 # rewrite one column, like numfmt --field
116
+ ```
117
+
118
+ Values come from the arguments or, when there are none, one per line from stdin.
119
+ Errors go to stderr with exit status 1. `bytesz --help` lists every option.
120
+
121
+ ## Why
122
+
123
+ Human byte sizes are a well known trap: `KB` means 1000 bytes to a disk vendor and
124
+ 1024 bytes to an operating system, and most libraries quietly pick one. The popular
125
+ options lean on guessing or only go one way: `humanize` and `hurry.filesize` format
126
+ numbers but do not parse them back, `humanfriendly` parses but treats `KB` and `KiB`
127
+ as the same 1024 based value, and `bitmath` mixes bits and bytes with a broad API.
128
+
129
+ `bytesz` is deliberately narrow and strict. The unit alone decides the system, both
130
+ directions are exact, and anything ambiguous or fractional is rejected with a clear
131
+ error instead of being rounded, unless you ask for rounding. Reach for it when a
132
+ wrong factor of 1024 would be a real bug, such as quotas, billing, or on-disk
133
+ accounting. With matching `--round` and `--format=%.Nf`, its formatting agrees with
134
+ GNU `numfmt` (checked in the test suite on seeded values), except where `numfmt` itself
135
+ drifts because it divides in floating point.
136
+
137
+ See [GUIDE.md](GUIDE.md) for the rules in depth.
138
+
139
+ ## License
140
+
141
+ MIT. See [LICENSE](LICENSE).
142
+
143
+ ## Links
144
+
145
+ - Website: https://lianes8studio.com
146
+ - GitHub: https://github.com/JaviLianes8
147
+ - LinkedIn: https://www.linkedin.com/in/jlianes/
bytesz-0.2.0/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # bytesz
2
+
3
+ Strict two-way conversion between byte counts and human sizes with explicit IEC and SI units.
4
+ Typed, exact (integer and `Decimal` arithmetic only), zero dependencies, with a small CLI.
5
+
6
+ ## Install
7
+
8
+ ```
9
+ pip install bytesz
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```python
15
+ from bytesz import format, parse
16
+
17
+ # Human size to an exact integer number of bytes. The unit picks the system.
18
+ parse("1.5 MiB") # 1572864 (IEC, 1024 based)
19
+ parse("1.5 MB") # 1500000 (SI, 1000 based)
20
+ parse("512 B") # 512
21
+ parse("2048") # 2048 (a bare number is bytes)
22
+
23
+ # Bytes back to a human size, for whichever system you ask for.
24
+ format(1572864) # "1.5 MiB" (IEC is the default)
25
+ format(1500000, system="si") # "1.5 MB"
26
+ format(1234567, system="si", precision=2) # "1.23 MB"
27
+ format(1048575) # "1 MiB" (rounding carries into the next unit)
28
+ ```
29
+
30
+ It refuses to guess:
31
+
32
+ ```python
33
+ from bytesz import AmbiguousUnitError, FractionalBytesError, SystemMismatchError, parse
34
+
35
+ parse("1 KB") # AmbiguousUnitError: KB could mean kB (1000) or KiB (1024)
36
+ parse("100 Mb") # AmbiguousUnitError: a lower case b means bits, see parse_bits
37
+ parse("1.5 B") # FractionalBytesError: not a whole number of bytes
38
+ parse("1 kB", system="iec") # SystemMismatchError: an SI unit under a pinned IEC system
39
+ ```
40
+
41
+ Every input error is a `ByteszError` (a `ValueError`) carrying `.text` and `.unit`. Bad
42
+ option values raise a plain `ValueError`, and a non-string input to `parse` a `TypeError`.
43
+
44
+ ## Options
45
+
46
+ ```python
47
+ from bytesz import convert, format, format_bits, parse, parse_bits
48
+
49
+ # Formatting
50
+ format(1250, system="si", precision=1, rounding="half-even") # "1.2 kB"
51
+ format(1_000_000, unit="MiB") # "0.95 MiB"
52
+ format(5000, unit="kB") # "5 kB" (the unit picks the system)
53
+ format(-1536, signed=True) # "-1.5 KiB"
54
+ format(1536, precision=3, trailing_zeros=True) # "1.500 KiB"
55
+ format(1536, decimal_point=",", separator="") # "1,5KiB"
56
+
57
+ # Parsing
58
+ parse("1.0005 kB", rounding="half-up") # 1001
59
+ parse("1,5 MiB", decimal_point=",") # 1572864
60
+ parse("-2 KiB", signed=True) # -2048
61
+ parse("1.5M", system="si") # 1500000 (bare prefixes, as numfmt writes them)
62
+ parse("1.5Mi") # 1572864 (bare IEC prefix)
63
+ parse("4 GB", legacy=True) # 4294967296 (JEDEC style KB/MB/GB = 1024, opt-in)
64
+
65
+ # Bits, with the unambiguous "bit" symbol
66
+ parse_bits("100 Mbit") # 100000000
67
+ format_bits(1_500_000, system="si") # "1.5 Mbit"
68
+
69
+ # Exact value in one unit, no rounding at all
70
+ convert(1, "KiB") # Decimal("0.0009765625")
71
+ ```
72
+
73
+ Rounding modes: `half-up` (default, ties away from zero), `half-even`, `half-down`,
74
+ `up`, `down`, `ceiling`, `floor`. Units reach the 2022 SI prefixes `RB` and `QB` and
75
+ the IEC 80000-13:2025 prefixes `RiB` and `QiB`.
76
+
77
+ ## Command line
78
+
79
+ ```
80
+ bytesz format 1572864 # 1.5 MiB
81
+ bytesz format --system si 1234567 # 1.23 MB
82
+ bytesz parse "1.5 MiB" 2GB # 1572864, 2000000000
83
+ bytesz parse --legacy 4KB # 4096
84
+ du -b * | bytesz format --field 1 # rewrite one column, like numfmt --field
85
+ ```
86
+
87
+ Values come from the arguments or, when there are none, one per line from stdin.
88
+ Errors go to stderr with exit status 1. `bytesz --help` lists every option.
89
+
90
+ ## Why
91
+
92
+ Human byte sizes are a well known trap: `KB` means 1000 bytes to a disk vendor and
93
+ 1024 bytes to an operating system, and most libraries quietly pick one. The popular
94
+ options lean on guessing or only go one way: `humanize` and `hurry.filesize` format
95
+ numbers but do not parse them back, `humanfriendly` parses but treats `KB` and `KiB`
96
+ as the same 1024 based value, and `bitmath` mixes bits and bytes with a broad API.
97
+
98
+ `bytesz` is deliberately narrow and strict. The unit alone decides the system, both
99
+ directions are exact, and anything ambiguous or fractional is rejected with a clear
100
+ error instead of being rounded, unless you ask for rounding. Reach for it when a
101
+ wrong factor of 1024 would be a real bug, such as quotas, billing, or on-disk
102
+ accounting. With matching `--round` and `--format=%.Nf`, its formatting agrees with
103
+ GNU `numfmt` (checked in the test suite on seeded values), except where `numfmt` itself
104
+ drifts because it divides in floating point.
105
+
106
+ See [GUIDE.md](GUIDE.md) for the rules in depth.
107
+
108
+ ## License
109
+
110
+ MIT. See [LICENSE](LICENSE).
111
+
112
+ ## Links
113
+
114
+ - Website: https://lianes8studio.com
115
+ - GitHub: https://github.com/JaviLianes8
116
+ - LinkedIn: https://www.linkedin.com/in/jlianes/
@@ -0,0 +1,61 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "bytesz"
7
+ version = "0.2.0"
8
+ description = "Strict two-way conversion between byte counts and human sizes with explicit IEC and SI units"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Javier Lianes", email = "javier@lianes8studio.com" }]
14
+ keywords = ["bytes", "size", "human", "iec", "si", "kib", "mib", "numfmt", "parse", "format"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Environment :: Console",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Topic :: Utilities",
23
+ "Typing :: Typed",
24
+ ]
25
+ dependencies = []
26
+
27
+ [project.scripts]
28
+ bytesz = "bytesz._cli:main"
29
+
30
+ [project.optional-dependencies]
31
+ dev = ["ruff", "mypy", "pytest", "build", "twine"]
32
+
33
+ [project.urls]
34
+ Homepage = "https://lianes8studio.com"
35
+ Repository = "https://github.com/JaviLianes8/bytesz"
36
+ Issues = "https://github.com/JaviLianes8/bytesz/issues"
37
+ Changelog = "https://github.com/JaviLianes8/bytesz/blob/main/CHANGELOG.md"
38
+ LinkedIn = "https://www.linkedin.com/in/jlianes/"
39
+ Funding = "https://buymeacoffee.com/jlianesglrs"
40
+
41
+ [tool.hatch.build.targets.sdist]
42
+ exclude = ["IMPROVEMENTS-round*.md", ".github"]
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/bytesz"]
46
+
47
+ [tool.ruff]
48
+ line-length = 100
49
+ target-version = "py310"
50
+
51
+ [tool.ruff.lint]
52
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
53
+
54
+ [tool.mypy]
55
+ python_version = "3.10"
56
+ strict = true
57
+
58
+ [tool.pytest.ini_options]
59
+ testpaths = ["tests"]
60
+ pythonpath = ["src"]
61
+ addopts = "-q"
@@ -0,0 +1,3 @@
1
+ # Development dependencies: the package in editable mode plus its dev extras
2
+ # (ruff, mypy, pytest, build, twine). Install with: pip install -r requirements-dev.txt
3
+ -e .[dev]
@@ -0,0 +1,8 @@
1
+ # Local quality gate. Run before every commit. No CI runs in the cloud.
2
+ $ErrorActionPreference = "Stop"
3
+
4
+ ruff format --check .
5
+ ruff check .
6
+ mypy --strict src tests
7
+ pytest
8
+ python scripts/e2e_wheel.py