UnityManager 0.0.1a1__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,40 @@
1
+ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3
+
4
+ name: Python package
5
+
6
+ on:
7
+ push:
8
+ branches: [ "master" ]
9
+ pull_request:
10
+ branches: [ "master" ]
11
+
12
+ jobs:
13
+ build:
14
+
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
20
+
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - name: Set up Python ${{ matrix.python-version }}
24
+ uses: actions/setup-python@v3
25
+ with:
26
+ python-version: ${{ matrix.python-version }}
27
+ - name: Install dependencies
28
+ run: |
29
+ python -m pip install --upgrade pip
30
+ python -m pip install flake8 pytest
31
+ if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
32
+ - name: Lint with flake8
33
+ run: |
34
+ # stop the build if there are Python syntax errors or undefined names
35
+ flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
36
+ # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
37
+ flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
38
+ - name: Test with pytest
39
+ run: |
40
+ pytest
@@ -0,0 +1,224 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+ .env-*
90
+ .env*
91
+ .venv-*
92
+ .venv*
93
+
94
+ # pipenv
95
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
97
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
98
+ # install all needed dependencies.
99
+ # Pipfile.lock
100
+
101
+ # UV
102
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
103
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
104
+ # commonly ignored for libraries.
105
+ # uv.lock
106
+
107
+ # poetry
108
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
109
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
110
+ # commonly ignored for libraries.
111
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
112
+ # poetry.lock
113
+ # poetry.toml
114
+
115
+ # pdm
116
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
117
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
118
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
119
+ # pdm.lock
120
+ # pdm.toml
121
+ .pdm-python
122
+ .pdm-build/
123
+
124
+ # pixi
125
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
126
+ # pixi.lock
127
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
128
+ # in the .venv directory. It is recommended not to include this directory in version control.
129
+ .pixi
130
+
131
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
132
+ __pypackages__/
133
+
134
+ # Celery stuff
135
+ celerybeat-schedule
136
+ celerybeat.pid
137
+
138
+ # Redis
139
+ *.rdb
140
+ *.aof
141
+ *.pid
142
+
143
+ # RabbitMQ
144
+ mnesia/
145
+ rabbitmq/
146
+ rabbitmq-data/
147
+
148
+ # ActiveMQ
149
+ activemq-data/
150
+
151
+ # SageMath parsed files
152
+ *.sage.py
153
+
154
+ # Environments
155
+ .env
156
+ .envrc
157
+ .venv
158
+ env/
159
+ venv/
160
+ ENV/
161
+ env.bak/
162
+ venv.bak/
163
+
164
+ # Spyder project settings
165
+ .spyderproject
166
+ .spyproject
167
+
168
+ # Rope project settings
169
+ .ropeproject
170
+
171
+ # mkdocs documentation
172
+ /site
173
+
174
+ # mypy
175
+ .mypy_cache/
176
+ .dmypy.json
177
+ dmypy.json
178
+
179
+ # Pyre type checker
180
+ .pyre/
181
+
182
+ # pytype static type analyzer
183
+ .pytype/
184
+
185
+ # Cython debug symbols
186
+ cython_debug/
187
+
188
+ # PyCharm
189
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
190
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
191
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
192
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
193
+ # .idea/
194
+
195
+ # Abstra
196
+ # Abstra is an AI-powered process automation framework.
197
+ # Ignore directories containing user credentials, local state, and settings.
198
+ # Learn more at https://abstra.io/docs
199
+ .abstra/
200
+
201
+ # Visual Studio Code
202
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
203
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
204
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
205
+ # you could uncomment the following to ignore the entire vscode folder
206
+ # .vscode/
207
+ # Temporary file for partial code execution
208
+ tempCodeRunnerFile.py
209
+
210
+ # Ruff stuff:
211
+ .ruff_cache/
212
+
213
+ # PyPI configuration file
214
+ .pypirc
215
+
216
+ # Marimo
217
+ marimo/_static/
218
+ marimo/_lsp/
219
+ __marimo__/
220
+
221
+ # Streamlit
222
+ .streamlit/secrets.toml
223
+
224
+ .docs/*
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PlayGames-2020
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.
@@ -0,0 +1,249 @@
1
+ Metadata-Version: 2.4
2
+ Name: UnityManager
3
+ Version: 0.0.1a1
4
+ Summary: Utilities for formatting common units.
5
+ Project-URL: Homepage, https://github.com/PlayGames-2020/UnityManager
6
+ Project-URL: Repository, https://github.com/PlayGames-2020/UnityManager.git
7
+ Project-URL: Documentation, https://github.com/PlayGames-2020/UnityManager/blob/main/README.md
8
+ Project-URL: Authors, https://github.com/PlayGames-2020
9
+ Author-email: PlayGames-2020 <playgames16.01.2020@gmail.com>
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 PlayGames-2020
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: bytes,formatting,games,length,mass,money,units,volume
33
+ Classifier: Development Status :: 3 - Alpha
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: OS Independent
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3 :: Only
39
+ Classifier: Programming Language :: Python :: 3.9
40
+ Classifier: Programming Language :: Python :: 3.10
41
+ Classifier: Programming Language :: Python :: 3.11
42
+ Classifier: Programming Language :: Python :: 3.12
43
+ Classifier: Programming Language :: Python :: 3.13
44
+ Classifier: Programming Language :: Python :: 3.14
45
+ Requires-Python: >=3.9
46
+ Provides-Extra: dev
47
+ Requires-Dist: pytest>=8.4.2; extra == 'dev'
48
+ Description-Content-Type: text/markdown
49
+
50
+ # UnityManager
51
+
52
+ Utilities for formatting common units without runtime dependencies.
53
+
54
+ > **Status:** alpha (`0.0.1-alpha`). The API is still subject to change.
55
+
56
+ ## Requirements
57
+
58
+ - Python **3.9 or later**
59
+ - No runtime dependencies
60
+
61
+ ## Installation
62
+
63
+ ### GitHub
64
+
65
+ ``` bash
66
+ pip install git+https://github.com/PlayGames-2020/UnityManager.git
67
+ ```
68
+
69
+ or
70
+
71
+ ``` bash
72
+ pip install git+https://github.com/PlayGames-2020/UnityManager.git@master
73
+ ```
74
+
75
+ ### PyPI
76
+
77
+ ``` bash
78
+ pip install UnityManager
79
+ ```
80
+
81
+ To install the development dependencies:
82
+
83
+ ```bash
84
+ pip install "unitymanager[dev]"
85
+ ```
86
+
87
+ ## Quick start
88
+
89
+ ```python
90
+ from UnityManager import (
91
+ format_bytes,
92
+ format_capacity,
93
+ format_length,
94
+ format_mass,
95
+ format_volume,
96
+ )
97
+ from games import format_money
98
+
99
+ print(format_bytes(1024)) # 1.00 KB
100
+ print(format_length(1500)) # 1.50 km
101
+ print(format_capacity(2.5)) # 2.50 l
102
+ print(format_mass(2500)) # 2.50 kg
103
+ print(format_volume(1)) # 1.00 m³
104
+ print(format_money(1500)) # 1.50 K
105
+ ```
106
+
107
+ Each formatter accepts a text mode. The default mode `"s"` displays the unit symbol. Use `"n"` for the unit name, combine modes such as `"ns"`, or add `"upper"`, `"lower"`, `"title"` and `"v"` as needed.
108
+
109
+ ## Games package
110
+
111
+ The optional game-oriented API is available from the `games` package:
112
+
113
+ ```python
114
+ from games import format_money
115
+
116
+ format_money(999) # "999.00"
117
+ format_money(1_500) # "1.50 K"
118
+ format_money(2_000_000) # "2.00 M"
119
+ format_money(1_500, "n") # "1.50 thousand"
120
+ format_money(1_500, "ns") # "1.50 thousand K"
121
+ format_money(1_500, "v") # "1.50 (1500 gold)"
122
+ format_money(1_500, "nsupper") # "1.50 THOUSAND K"
123
+ ```
124
+
125
+ ### `format_money(value, mode="s")`
126
+
127
+ Formats a numeric amount in the base game currency, assumed to be gold. Values
128
+ are rounded to two decimal places and use the largest applicable suffix:
129
+
130
+ | Range | Name | Symbol |
131
+ | ---: | --- | :--- |
132
+ | 1,000 | thousand | `K` |
133
+ | 1,000,000 | million | `M` |
134
+ | 1,000,000,000 | billion | `B` |
135
+ | 1,000,000,000,000 | trillion | `T` |
136
+ | 1,000,000,000,000,000 | quadrillion | `Q` |
137
+
138
+ Supported mode components:
139
+
140
+ - `s`: append the compact symbol.
141
+ - `n`: append the full unit name.
142
+ - `d`: append the unit description (reserved for future descriptions).
143
+ - `v`: append the original value as gold.
144
+ - `upper`, `lower`, `title`: change the case of the appended text.
145
+
146
+ Modes can be combined, for example `"nsupper"`. Non-numeric values are returned
147
+ as text instead of raising an exception.
148
+
149
+ The formatter is intentionally currency-neutral in its arithmetic: it only
150
+ compacts the amount. The displayed base-currency label is currently `gold` and
151
+ can be extended in a future API version if games need coins, gems, credits or
152
+ other currencies.
153
+
154
+ ## UnityManager API
155
+
156
+ All public formatters return a string rounded to two decimal places. Numeric
157
+ inputs use the base unit described below; non-numeric inputs are returned using
158
+ `str(value)`.
159
+
160
+ ### `format_bytes(value, mode="s")`
161
+
162
+ Formats binary quantities using powers of 1024. The base input is bytes and the
163
+ available symbols include `B`, `KB`, `MB`, `GB`, `TB`, `PB`, `EB` and `ZB`.
164
+ The mode `"byte"` adds the original value in bytes, while `"bit"` adds its
165
+ corresponding bit value.
166
+
167
+ ```python
168
+ format_bytes(1024) # "1.00 KB"
169
+ format_bytes(1024, "n") # "1.00 kilobyte"
170
+ format_bytes(2048, "sbyte") # "2.00 KB (2048 bytes)"
171
+ ```
172
+
173
+ ### `format_length(value, mode="s")`
174
+
175
+ Formats a value expressed in metres using metric units from millimetres to
176
+ kilometres.
177
+
178
+ ```python
179
+ format_length(1_500) # "1.50 km"
180
+ format_length(1, "n") # "1.00 metre"
181
+ format_length(1, "nv") # "1.00 metre (1.0 m)"
182
+ ```
183
+
184
+ ### `format_capacity(value, mode="s")`
185
+
186
+ Formats a value expressed in litres, from millilitres to kilolitres.
187
+
188
+ ```python
189
+ format_capacity(2.5) # "2.50 l"
190
+ format_capacity(1_000, "n") # "1.00 kilolitre"
191
+ ```
192
+
193
+ ### `format_mass(value, mode="s")`
194
+
195
+ Formats a value expressed in grams, from milligrams to kilograms.
196
+
197
+ ```python
198
+ format_mass(2_500) # "2.50 kg"
199
+ format_mass(1, "n") # "1.00 gram"
200
+ ```
201
+
202
+ ### `format_volume(value, mode="s")`
203
+
204
+ Formats a value expressed in cubic metres, from cubic millimetres to cubic
205
+ kilometres.
206
+
207
+ ```python
208
+ format_volume(1) # "1.00 m³"
209
+ format_volume(0.000001) # "1.00 cm³"
210
+ ```
211
+
212
+ ### Text modes
213
+
214
+ The general formatters support these mode components:
215
+
216
+ - `s`: append the unit symbol (the default).
217
+ - `n`: append the full unit name.
218
+ - `d`: append the unit description, when available.
219
+ - `v`: append the original value in the base unit.
220
+ - `upper`, `lower`, `title`: change the case of the appended text.
221
+
222
+ Modes can be combined as a string, for example `"nsupper"`.
223
+
224
+ ## Supported formatters
225
+
226
+ - `format_bytes`: binary byte units (`B`, `KB`, `MB`, and so on).
227
+ - `format_length`: metric lengths from millimetres to kilometres.
228
+ - `format_capacity`: metric capacities from millilitres to kilolitres.
229
+ - `format_mass`: metric masses from milligrams to kilograms.
230
+ - `format_volume`: cubic metric units from cubic millimetres to cubic kilometres.
231
+ - `games.format_money`: compact game currency values (`K`, `M`, `B`, `T`, `Q`).
232
+
233
+ ## Development and testing
234
+
235
+ ```bash
236
+ git clone https://github.com/PlayGames-2020/UnityManager.git
237
+ cd UnityManager
238
+ pip install -e ".[dev]"
239
+ python -m pytest
240
+ ```
241
+
242
+ ## License
243
+
244
+ Distributed under the [MIT License](LICENSE).
245
+
246
+ ## Links
247
+
248
+ - [Repository](https://github.com/PlayGames-2020/UnityManager)
249
+ - [Documentation](https://github.com/PlayGames-2020/UnityManager/blob/main/README.md)
@@ -0,0 +1,200 @@
1
+ # UnityManager
2
+
3
+ Utilities for formatting common units without runtime dependencies.
4
+
5
+ > **Status:** alpha (`0.0.1-alpha`). The API is still subject to change.
6
+
7
+ ## Requirements
8
+
9
+ - Python **3.9 or later**
10
+ - No runtime dependencies
11
+
12
+ ## Installation
13
+
14
+ ### GitHub
15
+
16
+ ``` bash
17
+ pip install git+https://github.com/PlayGames-2020/UnityManager.git
18
+ ```
19
+
20
+ or
21
+
22
+ ``` bash
23
+ pip install git+https://github.com/PlayGames-2020/UnityManager.git@master
24
+ ```
25
+
26
+ ### PyPI
27
+
28
+ ``` bash
29
+ pip install UnityManager
30
+ ```
31
+
32
+ To install the development dependencies:
33
+
34
+ ```bash
35
+ pip install "unitymanager[dev]"
36
+ ```
37
+
38
+ ## Quick start
39
+
40
+ ```python
41
+ from UnityManager import (
42
+ format_bytes,
43
+ format_capacity,
44
+ format_length,
45
+ format_mass,
46
+ format_volume,
47
+ )
48
+ from games import format_money
49
+
50
+ print(format_bytes(1024)) # 1.00 KB
51
+ print(format_length(1500)) # 1.50 km
52
+ print(format_capacity(2.5)) # 2.50 l
53
+ print(format_mass(2500)) # 2.50 kg
54
+ print(format_volume(1)) # 1.00 m³
55
+ print(format_money(1500)) # 1.50 K
56
+ ```
57
+
58
+ Each formatter accepts a text mode. The default mode `"s"` displays the unit symbol. Use `"n"` for the unit name, combine modes such as `"ns"`, or add `"upper"`, `"lower"`, `"title"` and `"v"` as needed.
59
+
60
+ ## Games package
61
+
62
+ The optional game-oriented API is available from the `games` package:
63
+
64
+ ```python
65
+ from games import format_money
66
+
67
+ format_money(999) # "999.00"
68
+ format_money(1_500) # "1.50 K"
69
+ format_money(2_000_000) # "2.00 M"
70
+ format_money(1_500, "n") # "1.50 thousand"
71
+ format_money(1_500, "ns") # "1.50 thousand K"
72
+ format_money(1_500, "v") # "1.50 (1500 gold)"
73
+ format_money(1_500, "nsupper") # "1.50 THOUSAND K"
74
+ ```
75
+
76
+ ### `format_money(value, mode="s")`
77
+
78
+ Formats a numeric amount in the base game currency, assumed to be gold. Values
79
+ are rounded to two decimal places and use the largest applicable suffix:
80
+
81
+ | Range | Name | Symbol |
82
+ | ---: | --- | :--- |
83
+ | 1,000 | thousand | `K` |
84
+ | 1,000,000 | million | `M` |
85
+ | 1,000,000,000 | billion | `B` |
86
+ | 1,000,000,000,000 | trillion | `T` |
87
+ | 1,000,000,000,000,000 | quadrillion | `Q` |
88
+
89
+ Supported mode components:
90
+
91
+ - `s`: append the compact symbol.
92
+ - `n`: append the full unit name.
93
+ - `d`: append the unit description (reserved for future descriptions).
94
+ - `v`: append the original value as gold.
95
+ - `upper`, `lower`, `title`: change the case of the appended text.
96
+
97
+ Modes can be combined, for example `"nsupper"`. Non-numeric values are returned
98
+ as text instead of raising an exception.
99
+
100
+ The formatter is intentionally currency-neutral in its arithmetic: it only
101
+ compacts the amount. The displayed base-currency label is currently `gold` and
102
+ can be extended in a future API version if games need coins, gems, credits or
103
+ other currencies.
104
+
105
+ ## UnityManager API
106
+
107
+ All public formatters return a string rounded to two decimal places. Numeric
108
+ inputs use the base unit described below; non-numeric inputs are returned using
109
+ `str(value)`.
110
+
111
+ ### `format_bytes(value, mode="s")`
112
+
113
+ Formats binary quantities using powers of 1024. The base input is bytes and the
114
+ available symbols include `B`, `KB`, `MB`, `GB`, `TB`, `PB`, `EB` and `ZB`.
115
+ The mode `"byte"` adds the original value in bytes, while `"bit"` adds its
116
+ corresponding bit value.
117
+
118
+ ```python
119
+ format_bytes(1024) # "1.00 KB"
120
+ format_bytes(1024, "n") # "1.00 kilobyte"
121
+ format_bytes(2048, "sbyte") # "2.00 KB (2048 bytes)"
122
+ ```
123
+
124
+ ### `format_length(value, mode="s")`
125
+
126
+ Formats a value expressed in metres using metric units from millimetres to
127
+ kilometres.
128
+
129
+ ```python
130
+ format_length(1_500) # "1.50 km"
131
+ format_length(1, "n") # "1.00 metre"
132
+ format_length(1, "nv") # "1.00 metre (1.0 m)"
133
+ ```
134
+
135
+ ### `format_capacity(value, mode="s")`
136
+
137
+ Formats a value expressed in litres, from millilitres to kilolitres.
138
+
139
+ ```python
140
+ format_capacity(2.5) # "2.50 l"
141
+ format_capacity(1_000, "n") # "1.00 kilolitre"
142
+ ```
143
+
144
+ ### `format_mass(value, mode="s")`
145
+
146
+ Formats a value expressed in grams, from milligrams to kilograms.
147
+
148
+ ```python
149
+ format_mass(2_500) # "2.50 kg"
150
+ format_mass(1, "n") # "1.00 gram"
151
+ ```
152
+
153
+ ### `format_volume(value, mode="s")`
154
+
155
+ Formats a value expressed in cubic metres, from cubic millimetres to cubic
156
+ kilometres.
157
+
158
+ ```python
159
+ format_volume(1) # "1.00 m³"
160
+ format_volume(0.000001) # "1.00 cm³"
161
+ ```
162
+
163
+ ### Text modes
164
+
165
+ The general formatters support these mode components:
166
+
167
+ - `s`: append the unit symbol (the default).
168
+ - `n`: append the full unit name.
169
+ - `d`: append the unit description, when available.
170
+ - `v`: append the original value in the base unit.
171
+ - `upper`, `lower`, `title`: change the case of the appended text.
172
+
173
+ Modes can be combined as a string, for example `"nsupper"`.
174
+
175
+ ## Supported formatters
176
+
177
+ - `format_bytes`: binary byte units (`B`, `KB`, `MB`, and so on).
178
+ - `format_length`: metric lengths from millimetres to kilometres.
179
+ - `format_capacity`: metric capacities from millilitres to kilolitres.
180
+ - `format_mass`: metric masses from milligrams to kilograms.
181
+ - `format_volume`: cubic metric units from cubic millimetres to cubic kilometres.
182
+ - `games.format_money`: compact game currency values (`K`, `M`, `B`, `T`, `Q`).
183
+
184
+ ## Development and testing
185
+
186
+ ```bash
187
+ git clone https://github.com/PlayGames-2020/UnityManager.git
188
+ cd UnityManager
189
+ pip install -e ".[dev]"
190
+ python -m pytest
191
+ ```
192
+
193
+ ## License
194
+
195
+ Distributed under the [MIT License](LICENSE).
196
+
197
+ ## Links
198
+
199
+ - [Repository](https://github.com/PlayGames-2020/UnityManager)
200
+ - [Documentation](https://github.com/PlayGames-2020/UnityManager/blob/main/README.md)
@@ -0,0 +1,41 @@
1
+ """Utilities for formatting common units.
2
+
3
+ The package exposes byte, length, capacity, mass and volume formatters at the
4
+ package level for convenient imports.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ import sys
11
+
12
+ from .bytes import format_bytes
13
+ from .length import format_length
14
+ from .measure import format_capacity, format_mass, format_volume
15
+
16
+ __version__ = "0.0.1-alpha1"
17
+ __author__ = "PlayGames-2020"
18
+ __summary__ = "Utilities for formatting common units."
19
+
20
+ _match = re.match(r"(\d+)\.(\d+)\.(\d+)(?:-(.+))?", __version__)
21
+
22
+ if _match: major, minor, patch, tag = _match.groups()
23
+ else: major, minor, patch, tag = 0, 0, 0, "unknown"
24
+
25
+ VERSION_INFO = (int(major), int(minor), int(patch), tag or "release")
26
+
27
+ NAME = __name__
28
+
29
+ _REQUIRED_PY = (3, 9)
30
+ if sys.version_info < (3, 9):
31
+ runtime = ".".join(str(v) for v in (_REQUIRED_PY))
32
+ info = f"{__name__} requires Python {runtime} or higher" \
33
+ f"(current: {sys.version.split()[0]})"
34
+ print(info)
35
+ raise RuntimeError(info)
36
+
37
+
38
+ __all__ = [
39
+ "__version__", "VERSION_INFO", "NAME",
40
+ "format_volume", "format_mass", "format_capacity", "format_length", "format_bytes"
41
+ ]
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Literal, Tuple, Union
4
+
5
+ _Mode = Union[str, Literal["n", "s", "d", "v", "lower", "upper", "title"]]
6
+ _TextMode = _Mode
7
+ _MeasureTextMode = _TextMode
8
+ _BytesTextMode = Union[str, Literal["s", "n", "d", "upper", "lower", "title", "byte", "bit"]]
9
+
10
+ _BYTES_TABLE: List[Tuple[str, str, Union[int, float], str]] = [
11
+ ("zettabyte", "ZB", 1024**7, ""),
12
+ ("exabyte", "EB", 1024**6, ""),
13
+ ("petabyte", "PB", 1024**5, ""),
14
+ ("terabyte", "TB", 1024**4, ""),
15
+ ("gigabyte", "GB", 1024**3, ""),
16
+ ("megabyte", "MB", 1024**2, ""),
17
+ ("kilobyte", "KB", 1024, ""),
18
+ ("byte", "B", 1, ""),
19
+ ("bit", "b", 1 / 8, ""),
20
+ ]
21
+
22
+ _LENGTH_TABLE: List[Tuple[str, str, float, str]] = [
23
+ ("kilometre", "km", 1000.0, ""),
24
+ ("hectometre", "hm", 100.0, ""),
25
+ ("decametre", "dam", 10.0, ""),
26
+ ("metre", "m", 1.0, ""),
27
+ ("decimetre", "dm", 0.1, ""),
28
+ ("centimetre", "cm", 0.01, ""),
29
+ ("millimetre", "mm", 0.001, ""),
30
+ ]
31
+
32
+ _CAPACITY_TABLE: List[Tuple[str, str, float, str]] = [
33
+ ("kilolitre", "kl", 1000.0, ""),
34
+ ("hectolitre", "hl", 100.0, ""),
35
+ ("decalitre", "dal", 10.0, ""),
36
+ ("litre", "l", 1.0, ""),
37
+ ("decilitre", "dl", 0.1, ""),
38
+ ("centilitre", "cl", 0.01, ""),
39
+ ("millilitre", "ml", 0.001, ""),
40
+ ]
41
+
42
+ _MASS_TABLE: List[Tuple[str, str, float, str]] = [
43
+ ("kilogram", "kg", 1000.0, ""),
44
+ ("hectogram", "hg", 100.0, ""),
45
+ ("decagram", "dag", 10.0, ""),
46
+ ("gram", "g", 1.0, ""),
47
+ ("decigram", "dg", 0.1, ""),
48
+ ("centigram", "cg", 0.01, ""),
49
+ ("milligram", "mg", 0.001, ""),
50
+ ]
51
+
52
+ _VOLUME_TABLE: List[Tuple[str, str, Union[int, float], str]] = [
53
+ ("kilometre cubic", "km³", 1_000_000_000.0, ""),
54
+ ("hectometre cubic", "hm³", 1_000_000.0, ""),
55
+ ("decametre cubic", "dam³", 1_000.0, ""),
56
+ ("metre cubic", "m³", 1.0, ""),
57
+ ("decimetre cubic", "dm³", 0.001, ""),
58
+ ("centimetre cubic", "cm³", 0.000_001, ""),
59
+ ("millimetre cubic", "mm³", 0.000_000_001, ""),
60
+ ]
@@ -0,0 +1,55 @@
1
+ """"""
2
+
3
+ from typing import Union, Any, List
4
+ from decimal import Decimal
5
+
6
+ from ._units import _BYTES_TABLE, _BytesTextMode
7
+
8
+ def format_bytes(n: Union[int, Any], mode: _BytesTextMode = "s") -> str:
9
+ """Format a byte value using binary units.
10
+
11
+ Values are divided by powers of 1024 and rounded to two decimals. The
12
+ default mode, ``"s"``, displays the unit symbol. ``"n"`` displays the
13
+ unit name, ``"d"`` its description, and ``"byte"`` or ``"bit"`` adds a
14
+ conversion note. Case modifiers can be combined with these modes.
15
+
16
+ Non-numeric values are returned as text.
17
+
18
+ Examples:
19
+ >>> format_bytes(1024)
20
+ '1.00 KB'
21
+ >>> format_bytes(1024, "n")
22
+ '1.00 kilobyte'
23
+ """
24
+ if not isinstance(n, (int, float, Decimal)): return str(n)
25
+
26
+ amount = Decimal(str(n))
27
+ abs_n = abs(amount)
28
+ unit_name: str
29
+ unit_symbol: str
30
+ unit_description: str
31
+ unit_name, unit_symbol, unit_description = "bit", "b", ""
32
+ for name, symbol, limit, description in _BYTES_TABLE:
33
+ _limit = Decimal(str(limit))
34
+ if abs_n >= limit:
35
+ amount = amount / _limit
36
+ unit_name, unit_symbol, unit_description = name, symbol, description
37
+ break
38
+
39
+ parts: List[str] = []
40
+
41
+ if "s" in mode: parts.append(unit_symbol)
42
+ if "n" in mode: parts.append(unit_name)
43
+ if "d" in mode: parts.append(unit_description)
44
+ if "upper" in mode: parts = [p.upper() for p in parts]
45
+ if "lower" in mode: parts = [p.lower() for p in parts]
46
+ if "title" in mode: parts = [p.title() for p in parts]
47
+ if "byte" in mode: parts.append(f"({n} bytes)")
48
+ if "bit" in mode: parts.append(f"({n / 8} bits)")
49
+
50
+ suffix = " ".join(parts)
51
+ return f"{amount:.2f}{f' {suffix}' if suffix else ''}"
52
+
53
+ __all__ = [
54
+ "format_bytes"
55
+ ]
@@ -0,0 +1,49 @@
1
+ """Format metric length quantities for display."""
2
+ from typing import Any, List, Literal, Union
3
+ from decimal import Decimal
4
+
5
+ from ._units import _LENGTH_TABLE, _TextMode
6
+ _LengthTextMode = _TextMode
7
+
8
+ def format_length(n: Union[int, Any], mode: _LengthTextMode = "s") -> str:
9
+ """Format a length in metric units, from millimetres to kilometres.
10
+
11
+ The input is interpreted as metres. The largest suitable unit is selected
12
+ automatically and the result is rounded to two decimal places. Use
13
+ ``"s"`` for the symbol, ``"n"`` for the name, ``"v"`` for the original
14
+ value, and combine these with ``"upper"``, ``"lower"`` or ``"title"``.
15
+
16
+ Non-numeric values are returned as text.
17
+ """
18
+ if not isinstance(n, (int, float, Decimal)): return str(n)
19
+
20
+ abs_n = abs(Decimal(str(n)))
21
+ amount = Decimal(str(n))
22
+ name, symbol, description = "metre", "m", ""
23
+
24
+ for name_item, symbol_item, limit, desc_item in _LENGTH_TABLE:
25
+ if limit >= 1 and abs_n >= Decimal(str(limit)):
26
+ amount = Decimal(str(n)) / Decimal(str(limit))
27
+ name, symbol, description = name_item, symbol_item, desc_item
28
+ break
29
+ elif limit < 1 and abs_n >= Decimal(str(limit)):
30
+ amount = Decimal(str(n)) / Decimal(str(limit))
31
+ name, symbol, description = name_item, symbol_item, desc_item
32
+ break
33
+
34
+ parts: List[str] = []
35
+ if "n" in mode: parts.append(name)
36
+ if "s" in mode: parts.append(symbol)
37
+ if "d" in mode: parts.append(description)
38
+ if "v" in mode: parts.append(f"({float(n)} m)")
39
+ if "lower" in mode: parts = [p.lower() for p in parts]
40
+ if "upper" in mode: parts = [p.upper() for p in parts]
41
+ if "title" in mode: parts = [p.title() for p in parts]
42
+
43
+ suffix = " ".join(parts).strip()
44
+ return f"{amount:.2f}{f' {suffix}' if suffix else ''}"
45
+
46
+
47
+ __all__ = [
48
+ "format_length"
49
+ ]
@@ -0,0 +1,49 @@
1
+ """Format capacity, mass and volume quantities for display."""
2
+ from typing import Any, List, Literal, Union, Tuple
3
+ from decimal import Decimal
4
+
5
+ from ._units import (
6
+ _CAPACITY_TABLE, _MASS_TABLE, _VOLUME_TABLE, _MeasureTextMode
7
+ )
8
+
9
+ def _generic_formatter(n: Union[int, Any], table: List[Tuple], default_unit: Tuple, base_symbol: str, mode: _MeasureTextMode = "s") -> str:
10
+ """Format a numeric measurement using the supplied unit table."""
11
+ if not isinstance(n, (int, float, Decimal)): return str(n)
12
+
13
+ abs_n = abs(Decimal(str(n)))
14
+ amount = Decimal(str(n))
15
+ name, symbol, description = default_unit
16
+
17
+ for name_item, symbol_item, limit, desc_item in table:
18
+ if abs_n >= Decimal(str(limit)):
19
+ amount = Decimal(str(n)) / Decimal(str(limit))
20
+ name, symbol, description = name_item, symbol_item, desc_item
21
+ break
22
+
23
+ parts: List[str] = []
24
+ if "n" in mode: parts.append(name)
25
+ if "s" in mode: parts.append(symbol)
26
+ if "d" in mode: parts.append(description)
27
+ if "v" in mode: parts.append(f"({float(n)} {base_symbol})")
28
+ if "lower" in mode: parts = [p.lower() for p in parts]
29
+ if "upper" in mode: parts = [p.upper() for p in parts]
30
+ if "title" in mode: parts = [p.title() for p in parts]
31
+
32
+ suffix = " ".join(parts).strip()
33
+ return f"{amount:.2f}{f' {suffix}' if suffix else ''}"
34
+
35
+ def format_capacity(n: Union[int, Any], mode: _MeasureTextMode = "s") -> str:
36
+ """Format a capacity expressed in litres."""
37
+ return _generic_formatter(n, _CAPACITY_TABLE, ("litre", "l", ""), "l", mode)
38
+
39
+ def format_mass(n: Union[int, Any], mode: _MeasureTextMode = "s") -> str:
40
+ """Format a mass expressed in grams."""
41
+ return _generic_formatter(n, _MASS_TABLE, ("gram", "g", ""), "g", mode)
42
+
43
+ def format_volume(n: Union[int, Any], mode: _MeasureTextMode = "s") -> str:
44
+ """Format a volume expressed in cubic metres."""
45
+ return _generic_formatter(n, _VOLUME_TABLE, ("metre cubic", "m³", ""), "m³", mode)
46
+
47
+ __all__ = [
48
+ "format_capacity", "format_mass", "format_volume"
49
+ ]
@@ -0,0 +1,11 @@
1
+ """Game-specific formatting helpers.
2
+
3
+ The public API currently provides :func:`format_money`, which formats large
4
+ in-game currency values using compact suffixes such as ``K`` and ``M``.
5
+ """
6
+
7
+ from .monetary import format_money
8
+
9
+ __all__ = [
10
+ "format_money"
11
+ ]
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Tuple, Union
4
+ from UnityManager._units import _TextMode
5
+
6
+ _MoneyTextMode = _TextMode
7
+
8
+ _MONEY_TABLE: List[Tuple[str, str, Union[int, float], str]] = [
9
+ ("quadrillion", "Q", 10 ** 15, ""),
10
+ ("trillion", "T", 10 ** 12, ""),
11
+ ("billion", "B", 10 ** 9, ""),
12
+ ("million", "M", 10 ** 6, ""),
13
+ ("thousand", "K", 10 ** 3, ""),
14
+ ("", "", 1, ""),
15
+ ]
@@ -0,0 +1,58 @@
1
+ """Formatting helpers for in-game monetary values."""
2
+ from typing import Any, List, Literal, Union
3
+ from decimal import Decimal
4
+
5
+ from ._units import _MONEY_TABLE, _MoneyTextMode
6
+
7
+ def format_money(n: Union[int, Any], mode: _MoneyTextMode = "s") -> str:
8
+ """Format an in-game currency amount with a compact suffix.
9
+
10
+ Args:
11
+ n: Numeric amount in the base currency (gold by default). Non-numeric
12
+ values are returned unchanged as text.
13
+ mode: Controls the text appended to the formatted number. ``"s"``
14
+ adds the suffix symbol, ``"n"`` adds its name, ``"d"`` adds the
15
+ description, and ``"v"`` shows the original amount as gold.
16
+ Case modifiers ``"upper"``, ``"lower"`` and ``"title"`` can be
17
+ combined with the other modes.
18
+
19
+ Returns:
20
+ A string containing the amount rounded to two decimal places and the
21
+ requested currency text.
22
+
23
+ Examples:
24
+ >>> format_money(1500)
25
+ '1.50 K'
26
+ >>> format_money(2_000_000, "n")
27
+ '2.00 million'
28
+ >>> format_money(1500, "nsupper")
29
+ '1.50 THOUSAND K'
30
+ """
31
+ if not isinstance(n, (int, float, Decimal)): return str(n)
32
+
33
+ abs_n = abs(Decimal(str(n)))
34
+ amount = Decimal(str(n))
35
+ name, symbol, description = "", "", ""
36
+
37
+ for name_item, symbol_item, limit, desc_item in _MONEY_TABLE:
38
+ if abs_n >= Decimal(str(limit)):
39
+ amount = Decimal(str(n)) / Decimal(str(limit))
40
+ name, symbol, description = name_item, symbol_item, desc_item
41
+ break
42
+
43
+ parts: List[str] = []
44
+
45
+ if "n" in mode: parts.append(name)
46
+ if "s" in mode: parts.append(symbol)
47
+ if "d" in mode: parts.append(description)
48
+ if "v" in mode: parts.append(f"({n} gold)")
49
+ if "lower" in mode: parts = [p.lower() for p in parts]
50
+ if "upper" in mode: parts = [p.upper() for p in parts]
51
+ if "title" in mode: parts = [p.title() for p in parts]
52
+
53
+ suffix = " ".join(part for part in parts if part)
54
+ return f"{amount:.2f}{f' {suffix}' if suffix else ''}"
55
+
56
+ __all__ = [
57
+ "format_money"
58
+ ]
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "UnityManager"
7
+ dynamic = ["version"]
8
+ author = "PlayGames-2020"
9
+ description = "Utilities for formatting common units."
10
+ readme = "README.md"
11
+ requires-python = ">=3.9"
12
+ authors = [
13
+ { name = "PlayGames-2020", email = "playgames16.01.2020@gmail.com" },
14
+ ]
15
+ license = { file = "LICENSE" }
16
+ keywords = ["units", "formatting", "bytes", "length", "mass", "volume", "games", "money"]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3 :: Only",
24
+ "Programming Language :: Python :: 3.9",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Programming Language :: Python :: 3.14",
30
+ ]
31
+ dependencies = []
32
+
33
+ [project.optional-dependencies]
34
+ dev = [
35
+ "pytest>=8.4.2"
36
+ ]
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/PlayGames-2020/UnityManager"
40
+ Repository = "https://github.com/PlayGames-2020/UnityManager.git"
41
+ Documentation = "https://github.com/PlayGames-2020/UnityManager/blob/main/README.md"
42
+ Authors = "https://github.com/PlayGames-2020"
43
+
44
+ [tool.hatch.version]
45
+ path = "UnityManager/__init__.py"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["UnityManager", "games"]
49
+ exclude = [
50
+ "*.pyc"
51
+ ]
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
File without changes
@@ -0,0 +1,25 @@
1
+ from UnityManager import (
2
+ format_bytes,
3
+ format_capacity,
4
+ format_length,
5
+ format_mass,
6
+ format_volume,
7
+ )
8
+
9
+
10
+ def test_public_api_formats_units():
11
+ assert format_bytes(1024) == "1.00 KB"
12
+ assert format_length(1000) == "1.00 km"
13
+ assert format_capacity(1000) == "1.00 kl"
14
+ assert format_mass(1000) == "1.00 kg"
15
+ assert format_volume(1) == "1.00 m³"
16
+
17
+
18
+ def test_formatters_support_names_and_case_modes():
19
+ assert format_length(1, "n") == "1.00 metre"
20
+ assert format_mass(1, "nupper") == "1.00 GRAM"
21
+
22
+
23
+ def test_non_numeric_values_are_returned_as_text():
24
+ assert format_bytes("unknown") == "unknown"
25
+ assert format_length(None) == "None"
@@ -0,0 +1,14 @@
1
+ from games import format_money
2
+
3
+
4
+ def test_money_uses_compact_game_currency_symbols():
5
+ assert format_money(999) == "999.00"
6
+ assert format_money(1_500) == "1.50 K"
7
+ assert format_money(2_000_000) == "2.00 M"
8
+ assert format_money(3_000_000_000) == "3.00 B"
9
+
10
+
11
+ def test_money_supports_names_and_original_value_mode():
12
+ assert format_money(1_500, "n") == "1.50 thousand"
13
+ assert format_money(1_500, "v") == "1.50 (1500 gold)"
14
+ assert format_money(1_500, "nsupper") == "1.50 THOUSAND K"