GJDutils 0.3.0__tar.gz → 0.3.4__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 (73) hide show
  1. {gjdutils-0.3.0 → gjdutils-0.3.4}/PKG-INFO +22 -11
  2. {gjdutils-0.3.0 → gjdutils-0.3.4}/README.md +11 -7
  3. {gjdutils-0.3.0 → gjdutils-0.3.4}/planning/250215_publishing_to_pypi.md +2 -2
  4. {gjdutils-0.3.0 → gjdutils-0.3.4}/pyproject.toml +12 -10
  5. {gjdutils-0.3.0 → gjdutils-0.3.4}/scripts/check_locally.py +3 -1
  6. {gjdutils-0.3.0 → gjdutils-0.3.4}/scripts/check_pypiprod.py +4 -2
  7. {gjdutils-0.3.0 → gjdutils-0.3.4}/scripts/check_pypitest.py +4 -2
  8. {gjdutils-0.3.0 → gjdutils-0.3.4}/scripts/deploy_all.py +3 -7
  9. {gjdutils-0.3.0 → gjdutils-0.3.4}/scripts/deploy_pypiprod.py +6 -6
  10. {gjdutils-0.3.0 → gjdutils-0.3.4}/scripts/deploy_pypitest.py +4 -3
  11. gjdutils-0.3.4/src/gjdutils/__init__.py +7 -0
  12. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/caching.py +1 -2
  13. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/dsci.py +13 -5
  14. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/env.py +2 -2
  15. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/files.py +2 -0
  16. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/functions.py +6 -6
  17. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/html.py +6 -7
  18. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/jsons.py +7 -3
  19. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/llm_utils.py +3 -3
  20. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/llms_claude.py +2 -3
  21. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/llms_openai.py +3 -5
  22. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/num.py +5 -5
  23. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/outloud_text_to_speech.py +10 -8
  24. gjdutils-0.3.0/src/gjdutils/vprint.py → gjdutils-0.3.4/src/gjdutils/print_utils.py +5 -2
  25. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/pypi_build.py +25 -10
  26. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/runtime.py +1 -1
  27. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/shell.py +0 -1
  28. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/strings.py +9 -2
  29. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/typ.py +3 -2
  30. gjdutils-0.3.4/tests/test_num.py +18 -0
  31. gjdutils-0.3.0/tests/test_vprint.py → gjdutils-0.3.4/tests/test_print_utils.py +1 -1
  32. gjdutils-0.3.0/__VERSION__.py +0 -1
  33. gjdutils-0.3.0/src/gjdutils/__init__.py +0 -12
  34. gjdutils-0.3.0/src/gjdutils/sorteddict.py +0 -34
  35. {gjdutils-0.3.0 → gjdutils-0.3.4}/.gitignore +0 -0
  36. {gjdutils-0.3.0 → gjdutils-0.3.4}/LICENSE +0 -0
  37. {gjdutils-0.3.0 → gjdutils-0.3.4}/docs/CODING_SHELL_SCRIPTS.md +0 -0
  38. {gjdutils-0.3.0 → gjdutils-0.3.4}/planning/250215_rename_gdutils_to_gjdutils.md +0 -0
  39. {gjdutils-0.3.0 → gjdutils-0.3.4}/scripts/check_git_clean.py +0 -0
  40. {gjdutils-0.3.0 → gjdutils-0.3.4}/scripts/export_all.sh +0 -0
  41. {gjdutils-0.3.0 → gjdutils-0.3.4}/scripts/install_all_dev_dependencies.py +0 -0
  42. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/audios.py +0 -0
  43. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/cmd.py +0 -0
  44. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/colab.py +0 -0
  45. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/collection_utils.py +0 -0
  46. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/decorators.py +0 -0
  47. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/dicts.py +0 -0
  48. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/dt.py +0 -0
  49. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/errors.py +0 -0
  50. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/google_translate.py +0 -0
  51. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/hashing.py +0 -0
  52. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/indexing.py +0 -0
  53. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/iterfunc.py +0 -0
  54. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/lists.py +0 -0
  55. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/misc.py +0 -0
  56. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/obsolete/google_text_to_speech.py +0 -0
  57. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/obsolete/llms_obsolete.py +0 -0
  58. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/prompt_templates.py +0 -0
  59. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/pytest_utils.py +0 -0
  60. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/rand.py +0 -0
  61. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/regex.py +0 -0
  62. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/requirements_dev.txt +0 -0
  63. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/sets.py +0 -0
  64. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/stopwatch.py +0 -0
  65. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/todo/convert_parquet.py +0 -0
  66. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/voice_speechrecognition.py +0 -0
  67. {gjdutils-0.3.0 → gjdutils-0.3.4}/src/gjdutils/web.py +0 -0
  68. {gjdutils-0.3.0 → gjdutils-0.3.4}/tests/__init__.py +0 -0
  69. {gjdutils-0.3.0 → gjdutils-0.3.4}/tests/test_env_integration.py +0 -0
  70. {gjdutils-0.3.0 → gjdutils-0.3.4}/tests/test_git_status.py +0 -0
  71. {gjdutils-0.3.0 → gjdutils-0.3.4}/tests/test_indexing.py +0 -0
  72. {gjdutils-0.3.0 → gjdutils-0.3.4}/tests/test_rand.py +0 -0
  73. {gjdutils-0.3.0 → gjdutils-0.3.4}/tests/test_strings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GJDutils
3
- Version: 0.3.0
3
+ Version: 0.3.4
4
4
  Summary: A collection of useful utility functions (basics, data science/AI, web development, etc)
5
5
  Project-URL: Homepage, https://github.com/gregdetre/gjdutils
6
6
  Project-URL: Repository, https://github.com/gregdetre/gjdutils
@@ -19,15 +19,18 @@ Requires-Dist: jinja2
19
19
  Requires-Dist: pydantic
20
20
  Requires-Dist: python-dotenv
21
21
  Provides-Extra: all-no-dev
22
+ Requires-Dist: anthropic; extra == 'all-no-dev'
22
23
  Requires-Dist: azure-cognitiveservices-speech; extra == 'all-no-dev'
23
24
  Requires-Dist: bs4; extra == 'all-no-dev'
24
- Requires-Dist: cachetools; extra == 'all-no-dev'
25
25
  Requires-Dist: elevenlabs; extra == 'all-no-dev'
26
26
  Requires-Dist: google-cloud-texttospeech; extra == 'all-no-dev'
27
27
  Requires-Dist: google-cloud-translate; extra == 'all-no-dev'
28
28
  Requires-Dist: humanize; extra == 'all-no-dev'
29
+ Requires-Dist: lxml; extra == 'all-no-dev'
30
+ Requires-Dist: numpy; extra == 'all-no-dev'
29
31
  Requires-Dist: openai; extra == 'all-no-dev'
30
32
  Requires-Dist: openai-whisper; extra == 'all-no-dev'
33
+ Requires-Dist: pandas; extra == 'all-no-dev'
31
34
  Requires-Dist: pendulum; extra == 'all-no-dev'
32
35
  Requires-Dist: pillow; extra == 'all-no-dev'
33
36
  Requires-Dist: playsound; extra == 'all-no-dev'
@@ -37,7 +40,6 @@ Requires-Dist: python-vlc; extra == 'all-no-dev'
37
40
  Requires-Dist: speechrecognition; extra == 'all-no-dev'
38
41
  Provides-Extra: audio-lang
39
42
  Requires-Dist: azure-cognitiveservices-speech; extra == 'audio-lang'
40
- Requires-Dist: cachetools; extra == 'audio-lang'
41
43
  Requires-Dist: elevenlabs; extra == 'audio-lang'
42
44
  Requires-Dist: google-cloud-texttospeech; extra == 'audio-lang'
43
45
  Requires-Dist: google-cloud-translate; extra == 'audio-lang'
@@ -50,18 +52,23 @@ Requires-Dist: speechrecognition; extra == 'audio-lang'
50
52
  Provides-Extra: dev
51
53
  Requires-Dist: black; extra == 'dev'
52
54
  Requires-Dist: build; extra == 'dev'
55
+ Requires-Dist: flake8; extra == 'dev'
53
56
  Requires-Dist: pytest; extra == 'dev'
54
57
  Requires-Dist: rich; extra == 'dev'
55
58
  Requires-Dist: twine; extra == 'dev'
56
59
  Requires-Dist: typer; extra == 'dev'
57
60
  Requires-Dist: wheel; extra == 'dev'
61
+ Provides-Extra: dsci
62
+ Requires-Dist: numpy; extra == 'dsci'
63
+ Requires-Dist: pandas; extra == 'dsci'
58
64
  Provides-Extra: dt
59
65
  Requires-Dist: humanize; extra == 'dt'
60
66
  Requires-Dist: pendulum; extra == 'dt'
61
67
  Provides-Extra: html-web
62
68
  Requires-Dist: bs4; extra == 'html-web'
69
+ Requires-Dist: lxml; extra == 'html-web'
63
70
  Provides-Extra: llm
64
- Requires-Dist: cachetools; extra == 'llm'
71
+ Requires-Dist: anthropic; extra == 'llm'
65
72
  Requires-Dist: openai; extra == 'llm'
66
73
  Requires-Dist: pillow; extra == 'llm'
67
74
  Description-Content-Type: text/markdown
@@ -74,6 +81,8 @@ This is a smorgasbord of utility functions, patterns and convenient wrappers tha
74
81
 
75
82
  Probably many of these exist elsewhere in libraries - if so, please let me know, because I'd probably rather use something cleaner and better-maintained.
76
83
 
84
+ Caveat emptor: some of these are old, and haven't been tested in a while.
85
+
77
86
 
78
87
  ## Highlights
79
88
 
@@ -97,7 +106,7 @@ from pathlib import Path
97
106
  retcode, stdout, extra = run_cmd(
98
107
  "python --version", # you can also provide as a list-of-strings
99
108
  before_msg="Checking Python version...",
100
- fatal_msg="Some problem running Python", # will show up in red
109
+ fatal_msg="Some problem running Python", # will show up in red, sys.exit(1)
101
110
  verbose=0, # Run silently unless there's an error
102
111
  **{"timeout": 5} # Pass additional arguments to subprocess
103
112
  )
@@ -108,7 +117,7 @@ print(f"Ran command: {extra['cmd_str']}") # plus lots of other stuff stored
108
117
 
109
118
  ### Environment variables with type validation and helpful error messages
110
119
  ```bash
111
- $ source scripts/export_all.sh .env
120
+ $ python -m gjdutils.scripts.export_all .env
112
121
  ```
113
122
 
114
123
  ```python
@@ -135,6 +144,7 @@ from gjdutils.rand import set_seeds
135
144
  set_seeds(42) # Sets seeds for random, numpy, torch if available
136
145
  ```
137
146
 
147
+
138
148
  ### Call Claude/OpenAI APIs with function calling, image analysis & JSON support
139
149
  ```python
140
150
  from gjdutils.llms_claude import call_claude_gpt
@@ -142,7 +152,7 @@ from gjdutils.llm_utils import image_to_base64
142
152
 
143
153
  response, extra = call_claude_gpt(
144
154
  "What's in this image?",
145
- image_filens=["path/to/image.jpg"], # Can pass multiple images
155
+ image_filens=["path/to/image.jpg"],
146
156
  temperature=0.001
147
157
  )
148
158
  ```
@@ -175,11 +185,10 @@ uniformity = calc_proportion_identical(['a', 'a', 'a', 'b']) # Returns 0.75 (75
175
185
  ```
176
186
 
177
187
 
178
-
179
188
  ### Generate deterministic cache keys for complex Python objects
180
189
  ```python
181
190
  from gjdutils.caching import generate_mckey
182
- cache_key = generate_mckey("myprefix", {"user_id": 123, "action": "login"}) # Creates deterministic cache key
191
+ cache_key = generate_mckey("myprefix", {"a": 100, "b": "foo"}) # Creates deterministic cache key
183
192
  ```
184
193
 
185
194
 
@@ -188,7 +197,7 @@ cache_key = generate_mckey("myprefix", {"user_id": 123, "action": "login"}) # C
188
197
  from gjdutils.hashing import hash_readable
189
198
 
190
199
  # Same input always produces same hash, even across sessions
191
- config = {"model": "gpt-4", "temperature": 0.7}
200
+ config = {"foo": "bar"}
192
201
  cache_key = hash_readable(config) # e.g. "8f4e5d3..."
193
202
  ```
194
203
 
@@ -207,6 +216,7 @@ print(pretty)
207
216
  # </div>
208
217
  ```
209
218
 
219
+
210
220
  ### Debug by printing local variables, excluding noise
211
221
  ```python
212
222
  from gjdutils.misc import print_locals
@@ -220,6 +230,7 @@ def my_function(x, y):
220
230
  # Output: {'x': 1, 'y': 2, 'z': 3}
221
231
  ```
222
232
 
233
+
223
234
  ### Generate readable random IDs (no confusing characters)
224
235
  ```python
225
236
  from gjdutils.rand import gen_readable_rand_id
@@ -261,7 +272,7 @@ pip install -e ".[all_no_dev, dev]" # Install all optional dependencies
261
272
  Or if you're feeling lazy and can't remember that command, just use:
262
273
 
263
274
  ```bash
264
- python scripts/install_all_dev_dependencies.py
275
+ python -m gjdutils.scripts.install_all_dev_dependencies
265
276
  ```
266
277
 
267
278
 
@@ -6,6 +6,8 @@ This is a smorgasbord of utility functions, patterns and convenient wrappers tha
6
6
 
7
7
  Probably many of these exist elsewhere in libraries - if so, please let me know, because I'd probably rather use something cleaner and better-maintained.
8
8
 
9
+ Caveat emptor: some of these are old, and haven't been tested in a while.
10
+
9
11
 
10
12
  ## Highlights
11
13
 
@@ -29,7 +31,7 @@ from pathlib import Path
29
31
  retcode, stdout, extra = run_cmd(
30
32
  "python --version", # you can also provide as a list-of-strings
31
33
  before_msg="Checking Python version...",
32
- fatal_msg="Some problem running Python", # will show up in red
34
+ fatal_msg="Some problem running Python", # will show up in red, sys.exit(1)
33
35
  verbose=0, # Run silently unless there's an error
34
36
  **{"timeout": 5} # Pass additional arguments to subprocess
35
37
  )
@@ -40,7 +42,7 @@ print(f"Ran command: {extra['cmd_str']}") # plus lots of other stuff stored
40
42
 
41
43
  ### Environment variables with type validation and helpful error messages
42
44
  ```bash
43
- $ source scripts/export_all.sh .env
45
+ $ python -m gjdutils.scripts.export_all .env
44
46
  ```
45
47
 
46
48
  ```python
@@ -67,6 +69,7 @@ from gjdutils.rand import set_seeds
67
69
  set_seeds(42) # Sets seeds for random, numpy, torch if available
68
70
  ```
69
71
 
72
+
70
73
  ### Call Claude/OpenAI APIs with function calling, image analysis & JSON support
71
74
  ```python
72
75
  from gjdutils.llms_claude import call_claude_gpt
@@ -74,7 +77,7 @@ from gjdutils.llm_utils import image_to_base64
74
77
 
75
78
  response, extra = call_claude_gpt(
76
79
  "What's in this image?",
77
- image_filens=["path/to/image.jpg"], # Can pass multiple images
80
+ image_filens=["path/to/image.jpg"],
78
81
  temperature=0.001
79
82
  )
80
83
  ```
@@ -107,11 +110,10 @@ uniformity = calc_proportion_identical(['a', 'a', 'a', 'b']) # Returns 0.75 (75
107
110
  ```
108
111
 
109
112
 
110
-
111
113
  ### Generate deterministic cache keys for complex Python objects
112
114
  ```python
113
115
  from gjdutils.caching import generate_mckey
114
- cache_key = generate_mckey("myprefix", {"user_id": 123, "action": "login"}) # Creates deterministic cache key
116
+ cache_key = generate_mckey("myprefix", {"a": 100, "b": "foo"}) # Creates deterministic cache key
115
117
  ```
116
118
 
117
119
 
@@ -120,7 +122,7 @@ cache_key = generate_mckey("myprefix", {"user_id": 123, "action": "login"}) # C
120
122
  from gjdutils.hashing import hash_readable
121
123
 
122
124
  # Same input always produces same hash, even across sessions
123
- config = {"model": "gpt-4", "temperature": 0.7}
125
+ config = {"foo": "bar"}
124
126
  cache_key = hash_readable(config) # e.g. "8f4e5d3..."
125
127
  ```
126
128
 
@@ -139,6 +141,7 @@ print(pretty)
139
141
  # </div>
140
142
  ```
141
143
 
144
+
142
145
  ### Debug by printing local variables, excluding noise
143
146
  ```python
144
147
  from gjdutils.misc import print_locals
@@ -152,6 +155,7 @@ def my_function(x, y):
152
155
  # Output: {'x': 1, 'y': 2, 'z': 3}
153
156
  ```
154
157
 
158
+
155
159
  ### Generate readable random IDs (no confusing characters)
156
160
  ```python
157
161
  from gjdutils.rand import gen_readable_rand_id
@@ -193,7 +197,7 @@ pip install -e ".[all_no_dev, dev]" # Install all optional dependencies
193
197
  Or if you're feeling lazy and can't remember that command, just use:
194
198
 
195
199
  ```bash
196
- python scripts/install_all_dev_dependencies.py
200
+ python -m gjdutils.scripts.install_all_dev_dependencies
197
201
  ```
198
202
 
199
203
 
@@ -16,7 +16,7 @@
16
16
  1. Build and test package locally:
17
17
  ```bash
18
18
  # Option 1: Automated testing script (recommended)
19
- ./scripts/check_locally.sh
19
+ python -m gjdutils.scripts.check_locally
20
20
 
21
21
  # Option 2: Manual steps
22
22
  # Clean any existing builds
@@ -29,7 +29,7 @@
29
29
  2. Test PyPI Deployment:
30
30
  ```bash
31
31
  # Option 1: Automated testing script (recommended)
32
- ./scripts/check_test_pypi.sh
32
+ python -m gjdutils.scripts.check_test_pypi
33
33
 
34
34
  # Option 2: Manual steps
35
35
  # Upload to test.pypi.org
@@ -1,8 +1,7 @@
1
1
  [project]
2
2
  # case-insensitive
3
3
  name = "GJDutils"
4
- # https://hatch.pypa.io/latest/config/metadata/
5
- dynamic = ["version"]
4
+ version = "0.3.4"
6
5
  authors = [
7
6
  { name="Greg Detre", email="greg@gregdetre.com" },
8
7
  ]
@@ -31,7 +30,6 @@ dependencies = [
31
30
  [project.optional-dependencies]
32
31
  audio_lang = [
33
32
  "azure-cognitiveservices-speech",
34
- "cachetools",
35
33
  "google-cloud-texttospeech",
36
34
  "google-cloud-translate",
37
35
  "elevenlabs",
@@ -48,12 +46,17 @@ audio_lang = [
48
46
  ]
49
47
  dev = [
50
48
  "black",
51
- "pytest",
52
49
  "build",
53
- "twine",
54
- "wheel",
50
+ "flake8",
51
+ "pytest",
55
52
  "rich", # for console output formatting
53
+ "twine",
56
54
  "typer", # for CLI tools
55
+ "wheel",
56
+ ]
57
+ dsci = [
58
+ "numpy",
59
+ "pandas",
57
60
  ]
58
61
  dt = [
59
62
  "humanize",
@@ -61,9 +64,10 @@ dt = [
61
64
  ]
62
65
  html_web = [
63
66
  "bs4",
67
+ "lxml",
64
68
  ]
65
69
  llm = [
66
- "cachetools",
70
+ "anthropic",
67
71
  # "llm", no longer using this
68
72
  "openai",
69
73
  "pillow", # for images
@@ -71,6 +75,7 @@ llm = [
71
75
 
72
76
  all_no_dev = [
73
77
  "gjdutils[audio_lang]",
78
+ "gjdutils[dsci]",
74
79
  "gjdutils[dt]",
75
80
  "gjdutils[html_web]",
76
81
  "gjdutils[llm]",
@@ -94,6 +99,3 @@ exclude = [
94
99
  "gjdutils/todo/",
95
100
  "tests/fixme_*.py",
96
101
  ]
97
-
98
- [tool.hatch.version]
99
- path = "__VERSION__.py"
@@ -37,6 +37,9 @@ def install_and_test_package(python_path: Path, wheel_file: Path):
37
37
  fatal_msg="Failed to install package",
38
38
  )
39
39
 
40
+ # Install all optional dependencies first
41
+ check_install_optional_features(python_path)
42
+
40
43
  # Command: pip install ".[dev]"
41
44
  run_cmd(
42
45
  f"{python_path} -m pip install '.[dev]'",
@@ -65,7 +68,6 @@ def main():
65
68
  install_and_test_package(python_path, wheel_file)
66
69
  verify_installation(python_path)
67
70
  run_test_suite(python_path)
68
- check_install_optional_features(python_path)
69
71
 
70
72
  console.print("\nLocal testing completed successfully!", style="green")
71
73
 
@@ -13,12 +13,15 @@ console = Console()
13
13
 
14
14
  def install_from_pypiprod(python_path: Path):
15
15
  # Command: pip install gjdutils
16
- return run_cmd(
16
+ run_cmd(
17
17
  f"{python_path} -m pip install gjdutils",
18
18
  before_msg="Installing package from PyPI...",
19
19
  fatal_msg="Failed to install package from PyPI",
20
20
  )
21
21
 
22
+ # Install all optional dependencies
23
+ check_install_optional_features(python_path, from_test_pypi=False)
24
+
22
25
 
23
26
  def main():
24
27
  console.rule("[yellow]Starting Production PyPI package testing")
@@ -27,7 +30,6 @@ def main():
27
30
  with temp_venv(venv_path) as python_path:
28
31
  install_from_pypiprod(python_path)
29
32
  verify_installation(python_path)
30
- check_install_optional_features(python_path, from_test_pypi=False)
31
33
 
32
34
  console.print("\nProduction PyPI testing completed successfully!", style="green")
33
35
 
@@ -13,13 +13,16 @@ console = Console()
13
13
 
14
14
  def install_from_test_pypi(python_path: Path):
15
15
  # Command: pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ gjdutils
16
- return run_cmd(
16
+ run_cmd(
17
17
  f"{python_path} -m pip install --index-url https://test.pypi.org/simple/ "
18
18
  "--extra-index-url https://pypi.org/simple/ gjdutils",
19
19
  before_msg="Installing package from Test PyPI...",
20
20
  fatal_msg="Failed to install package from Test PyPI",
21
21
  )
22
22
 
23
+ # Install all optional dependencies
24
+ check_install_optional_features(python_path, from_test_pypi=True)
25
+
23
26
 
24
27
  def main():
25
28
  console.rule("[yellow]Starting Test PyPI package testing")
@@ -28,7 +31,6 @@ def main():
28
31
  with temp_venv(venv_path) as python_path:
29
32
  install_from_test_pypi(python_path)
30
33
  verify_installation(python_path)
31
- check_install_optional_features(python_path, from_test_pypi=True)
32
34
 
33
35
  console.print("\nTest PyPI testing completed successfully!", style="green")
34
36
 
@@ -2,11 +2,9 @@
2
2
 
3
3
  from rich.console import Console
4
4
 
5
- from check_locally import main as check_locally_main
6
- from deploy_pypitest import main as deploy_pypitest_main
7
- from check_pypitest import main as check_pypitest_main
8
- from deploy_pypiprod import main as deploy_pypiprod_main
9
- from check_pypiprod import main as check_pypiprod_main
5
+ from .check_locally import main as check_locally_main
6
+ from .deploy_pypitest import main as deploy_pypitest_main
7
+ from .deploy_pypiprod import main as deploy_pypiprod_main
10
8
 
11
9
  console = Console()
12
10
 
@@ -16,9 +14,7 @@ def main():
16
14
 
17
15
  check_locally_main()
18
16
  deploy_pypitest_main()
19
- check_pypitest_main()
20
17
  deploy_pypiprod_main()
21
- check_pypiprod_main()
22
18
 
23
19
  console.print("\n[green]Full deployment process completed successfully! 🎉[/green]")
24
20
 
@@ -1,11 +1,9 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
3
  from rich.console import Console
4
- from pathlib import Path
5
4
  from packaging.version import Version
6
5
 
7
6
  from gjdutils import __version__
8
- from gjdutils.decorators import console_print_doc
9
7
  from gjdutils.pypi_build import (
10
8
  check_version_exists,
11
9
  clean_build_dirs,
@@ -13,8 +11,9 @@ from gjdutils.pypi_build import (
13
11
  upload_to_pypi,
14
12
  )
15
13
  from gjdutils.shell import fatal_error_msg
16
- from gjdutils.cmd import run_cmd
17
- from scripts.check_git_clean import check_git_status
14
+ from .check_git_clean import check_git_status
15
+ from .check_pypiprod import main as check_pypiprod_main
16
+
18
17
 
19
18
  console = Console()
20
19
 
@@ -28,7 +27,7 @@ def main():
28
27
  # Check if version already exists
29
28
  if check_version_exists(Version(__version__), pypi_env="prod"):
30
29
  fatal_error_msg(
31
- f"Version {__version__} already exists on PyPI.\nPlease update __VERSION__.py to a new version number first."
30
+ f"Version {__version__} already exists on PyPI.\nPlease update the version in pyproject.toml to a new version number first."
32
31
  )
33
32
 
34
33
  # Confirm with user before proceeding
@@ -45,7 +44,8 @@ def main():
45
44
  upload_to_pypi(pypi_env="prod")
46
45
 
47
46
  console.print("\n[green]Deployment to Production PyPI completed![/green]")
48
- console.print("Run ./scripts/check_pypiprod.py to verify the deployment")
47
+
48
+ check_pypiprod_main()
49
49
 
50
50
 
51
51
  if __name__ == "__main__":
@@ -5,13 +5,13 @@ from pathlib import Path
5
5
  from packaging.version import Version
6
6
 
7
7
  from gjdutils import __version__
8
- from gjdutils.decorators import console_print_doc
9
8
  from gjdutils.pypi_build import (
10
9
  check_version_exists,
11
10
  clean_build_dirs,
12
11
  build_package,
13
12
  upload_to_pypi,
14
13
  )
14
+ from .check_pypitest import main as check_pypitest_main
15
15
  from gjdutils.shell import fatal_error_msg
16
16
 
17
17
  console = Console()
@@ -23,7 +23,7 @@ def main():
23
23
  # Check if version already exists
24
24
  if check_version_exists(Version(__version__), pypi_env="test"):
25
25
  fatal_error_msg(
26
- f"Version {__version__} already exists on Test PyPI.\nPlease update __VERSION__.py to a new version number first."
26
+ f"Version {__version__} already exists on Test PyPI.\nPlease update the version in pyproject.toml to a new version number first."
27
27
  )
28
28
 
29
29
  # Execute deployment steps
@@ -32,7 +32,8 @@ def main():
32
32
  upload_to_pypi(pypi_env="test")
33
33
 
34
34
  console.print("\n[green]Deployment to Test PyPI completed![/green]")
35
- console.print("Run ./scripts/check_pypitest.py to verify the deployment")
35
+
36
+ check_pypitest_main()
36
37
 
37
38
 
38
39
  if __name__ == "__main__":
@@ -0,0 +1,7 @@
1
+ """
2
+ GJDutils - A collection of useful utility functions
3
+ """
4
+
5
+ from gjdutils.pypi_build import get_version
6
+
7
+ __version__ = get_version()
@@ -2,7 +2,6 @@ from functools import update_wrapper
2
2
  import inspect
3
3
  import re
4
4
 
5
- from gjdutils.sorteddict import SortedDict
6
5
  from gjdutils.typ import isiterable
7
6
 
8
7
 
@@ -51,7 +50,7 @@ def generate_mckey(prefix, d):
51
50
  The idea is to ensure that no matter how D was
52
51
  created, you'll know what the key should be.
53
52
  """
54
- sorted_d = SortedDict(lambda x: x)
53
+ sorted_d = dict()
55
54
  for k in sorted(d.keys()):
56
55
  sorted_d[k] = d[k]
57
56
  return sorted_d
@@ -1,16 +1,16 @@
1
1
  from collections import Counter
2
2
  import itertools
3
- import numpy as np
4
- import pandas as pd
5
- import random
6
- from scipy import spatial, sparse
7
3
  import sys
8
- from typing import Any, Iterable, Sequence
4
+ from typing import Any, Sequence
9
5
 
6
+ # keep this, because it makes sense for the user to be able to import from here
10
7
  from .rand import set_seeds
11
8
 
12
9
 
13
10
  def init_display_options():
11
+ import numpy as np
12
+ import pandas as pd
13
+
14
14
  # so that it's easier to see things in the terminal
15
15
  pd.set_option("display.max_rows", 10000)
16
16
  pd.set_option("display.max_columns", 1000)
@@ -74,6 +74,8 @@ def calc_normalised_std_tightness(vals: Sequence[float]) -> float:
74
74
  [19, 1, 40, 20] -> 0.31
75
75
  [ 9, 1, 70, 0] -> 0
76
76
  """
77
+ import numpy as np
78
+
77
79
  n = len(vals)
78
80
  if n == 0:
79
81
  raise Exception("Empty")
@@ -143,6 +145,8 @@ def convert_sim_dist_oneminus(val: float) -> float:
143
145
 
144
146
 
145
147
  def square_df_from_square(sq, features):
148
+ import pandas as pd
149
+
146
150
  df = pd.DataFrame(sq)
147
151
  # create a 'Feature' column
148
152
  df["Feature"] = features
@@ -154,6 +158,8 @@ def square_df_from_square(sq, features):
154
158
 
155
159
 
156
160
  def long_df_from_flat(dists_flat, features):
161
+ import pandas as pd
162
+
157
163
  combos = [(f1, f2) for f1, f2 in itertools.permutations(features, 2)]
158
164
  assert len(combos) == len(dists_flat)
159
165
  dists_triplet = [
@@ -164,6 +170,8 @@ def long_df_from_flat(dists_flat, features):
164
170
 
165
171
 
166
172
  def square_df_from_flat(dists_flat, features):
173
+ from scipy import spatial
174
+
167
175
  nFeatures = len(features)
168
176
  dists_sq = spatial.distance.squareform(np.array(dists_flat))
169
177
  assert dists_sq.shape == (nFeatures, nFeatures)
@@ -3,13 +3,13 @@ from pathlib import Path
3
3
  from typing import Any, TypeVar, cast
4
4
  from pydantic import StrictStr, TypeAdapter
5
5
 
6
- from gjdutils.vprint import vprint
6
+ from gjdutils.print_utils import vprint
7
7
 
8
8
  T = TypeVar("T")
9
9
  _processed_vars = set()
10
10
 
11
11
 
12
- # You may find it useful to run `scripts/export_all.sh .env` to first
12
+ # You may find it useful to run `python -m gjdutils.scripts.export_all .env` to first
13
13
  # export all the variables in your .env file to your environment.
14
14
 
15
15
 
@@ -3,6 +3,8 @@ from pathlib import Path
3
3
  from typing import Sequence
4
4
 
5
5
  from .cmd import run_cmd
6
+
7
+ # keep this, because it makes sense for the user to be able to import from here
6
8
  from .strings import is_string, PathOrStr
7
9
 
8
10
 
@@ -4,7 +4,7 @@ from typing import Any
4
4
 
5
5
  def func_name():
6
6
  # https://stackoverflow.com/a/13514318/230523
7
- return inspect.currentframe().f_back.f_code.co_name
7
+ return inspect.currentframe().f_back.f_code.co_name # type: ignore
8
8
 
9
9
 
10
10
  def variable_from_caller(var_name: str, frame_depth: int = 1) -> Any:
@@ -21,18 +21,18 @@ def variable_from_caller(var_name: str, frame_depth: int = 1) -> Any:
21
21
  try:
22
22
  # Go back the specified number of frames
23
23
  for _ in range(frame_depth + 1):
24
- if frame.f_back is None:
24
+ if frame.f_back is None: # type: ignore
25
25
  raise ValueError(f"Cannot go back {frame_depth} frames")
26
- frame = frame.f_back
26
+ frame = frame.f_back # type: ignore
27
27
 
28
- if var_name not in frame.f_locals:
29
- caller_name = frame.f_code.co_name
28
+ if var_name not in frame.f_locals: # type: ignore
29
+ caller_name = frame.f_code.co_name # type: ignore
30
30
  raise ValueError(
31
31
  f"Variable '{var_name}' not found in caller function '{caller_name}'. "
32
32
  f"Make sure to define a '{var_name}' parameter or variable."
33
33
  )
34
34
 
35
- return frame.f_locals[var_name]
35
+ return frame.f_locals[var_name] # type: ignore
36
36
  finally:
37
37
  # Clean up circular references
38
38
  del frame
@@ -1,8 +1,7 @@
1
1
  from bs4 import BeautifulSoup
2
- from lxml import html as lxml_html
3
- from lxml.html import tostring, fromstring
4
- from lxml.etree import Element, _Element as ElementType
5
- from typing import Optional, Iterable, Union
2
+ from lxml.html import tostring
3
+ from lxml.etree import _Element as ElementType
4
+ from typing import Optional, Union
6
5
 
7
6
 
8
7
  def remove_html_tags(html: str):
@@ -65,15 +64,15 @@ def prettify_html(
65
64
  if isinstance(html, list):
66
65
  # then we'll handle it as a string in a moment
67
66
  html = "".join(
68
- [tostring(e, method="html").decode() for e in html]
67
+ [tostring(e, method="html").decode() for e in html] # type: ignore
69
68
  ) #  type: ignore
70
69
  if isinstance(html, ElementType):
71
70
  # this will do some cleaning and fixing. but
72
71
  # you need document_fromstring() if you want to make sure
73
72
  # that it's a full html doc, e.g. with html, body
74
- html = tostring(html, method="html").decode()
73
+ html = tostring(html, method="html").decode() # type: ignore
75
74
 
76
- soup = BeautifulSoup(html, "html.parser")
75
+ soup = BeautifulSoup(html, "html.parser") # type: ignore
77
76
  # html2 = tostring(html, pretty_print=True, method="html").decode() # type: ignore
78
77
  # the lxml pretty_print just isn't as good as BS4, e.g. with a list of elements
79
78
  # it wraps things in a div fragment, but the pretty-print of that isn't right
@@ -6,10 +6,12 @@ def jsonify(x):
6
6
  def json_dumper_robust(obj):
7
7
  try:
8
8
  return obj.toJSON()
9
- except:
9
+ except AttributeError:
10
+ # Object doesn't have toJSON method, try string conversion
10
11
  try:
11
12
  return str(obj)
12
- except:
13
+ except (ValueError, TypeError):
14
+ # If string conversion fails, return None to skip this field
13
15
  return None
14
16
 
15
17
  return json.dumps(x, sort_keys=True, indent=4, default=json_dumper_robust)
@@ -49,6 +51,8 @@ def to_json(
49
51
  Convert a list of dicts to a JSON string, with only the fields we want,
50
52
  and in the same order as FIELDS.
51
53
  """
54
+ if fields is None:
55
+ fields = []
52
56
  outs = []
53
57
  for inp in inps:
54
58
  if fields is None:
@@ -56,7 +60,7 @@ def to_json(
56
60
  # we want to make sure to return a dict with only the fields we want,
57
61
  # and in the same order as FIELDS
58
62
  out = {}
59
- for k in fields:
63
+ for k in fields: # type: ignore
60
64
  if skip_if_missing and (k not in inp):
61
65
  continue
62
66
  v = inp[k] # will error if missing and !SKIP_IF_MISSING
@@ -33,9 +33,9 @@ def proc_llm_out_json(s: str):
33
33
  s = s[7:-3]
34
34
  try:
35
35
  j = json.loads(s)
36
- except:
37
- print(s)
38
- raise Exception("Could not parse JSON from LLM output")
36
+ except json.JSONDecodeError:
37
+ print("Failed to parse JSON:", s)
38
+ raise
39
39
  return j
40
40
 
41
41
 
@@ -1,13 +1,12 @@
1
1
  import json
2
2
  from pathlib import Path
3
3
  from anthropic import Anthropic, NOT_GIVEN
4
- import os
5
4
  from typing import Optional
6
5
 
7
- from gjdutils.rand import DEFAULT_RANDOM_SEED
8
6
  from gjdutils.llm_utils import image_to_base64_basic
7
+ from gjdutils.env import get_env_var
9
8
 
10
- CLAUDE_API_KEY = os.environ.get("CLAUDE_API_KEY")
9
+ CLAUDE_API_KEY = get_env_var("CLAUDE_API_KEY")
11
10
  # https://docs.anthropic.com/en/docs/about-claude/models
12
11
  MODEL_NAME_CLAUDE_SONNET_GOOD_PINNED = "claude-3-5-sonnet-20240620"
13
12
  MODEL_NAME_CLAUDE_SONNET_GOOD_LATEST = "claude-3-5-sonnet-latest"
@@ -1,17 +1,15 @@
1
1
  import json
2
2
  from openai import OpenAI, NOT_GIVEN
3
- import os
4
- from pprint import pprint
5
- from typing import Any, Literal, Optional
3
+ from typing import Literal, Optional
6
4
 
7
5
  from .prompt_templates import summarise_list_of_texts_as_one, summarise_text
8
6
  from .rand import DEFAULT_RANDOM_SEED
9
7
  from .strings import jinja_render
10
8
  from gjdutils.llm_utils import contents_for_images
9
+ from gjdutils.env import get_env_var
11
10
 
12
11
 
13
- # openai.api_key = OPENAI_API_KEY
14
- OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
12
+ OPENAI_API_KEY = get_env_var("OPENAI_API_KEY")
15
13
  MODEL_NAME_GPT4 = "gpt-4"
16
14
  MODEL_NAME_GPT35 = "gpt-3.5-turbo"
17
15
  MODEL_NAME_GPT4_TURBO = "gpt-4-turbo" # -1106-preview"
@@ -20,7 +20,7 @@ def discretise(
20
20
  increment: Union[int, float] = 0.1,
21
21
  lower: Union[int, float] = 0.0,
22
22
  upper: Union[int, float] = 1.0,
23
- enforce_range: bool = False,
23
+ enforce_range: bool = True,
24
24
  ):
25
25
  """
26
26
  You will probably want to cache this.
@@ -40,10 +40,10 @@ def discretise(
40
40
 
41
41
  if pd.isnull(val):
42
42
  return upper
43
- if enforce_range:
44
- assert (
45
- lower <= val <= upper
46
- ), f"Required: {lower:.2f} < {val:.2f} <= {upper:.2f}"
43
+ if enforce_range and (val < lower or val > upper):
44
+ raise ValueError(
45
+ f"Value {val:.2f} is outside the valid range [{lower:.2f}, {upper:.2f}]"
46
+ )
47
47
  increments = calc_increments(increment, lower, upper)
48
48
  if val < lower:
49
49
  return increments[0]
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  from typing import Optional
3
+ from gjdutils.env import get_env_var
3
4
 
4
5
 
5
6
  def outloud(
@@ -33,11 +34,12 @@ def outloud(
33
34
  verbose=verbose,
34
35
  )
35
36
  elif prog == "elevenlabs":
37
+ assert speed is None, "speed not supported for elevenlabs"
36
38
  response = outloud_elevenlabs(
37
39
  text=text,
38
40
  mp3_filen=mp3_filen,
39
41
  bot_name=bot_name,
40
- speed=speed,
42
+ # speed=speed,
41
43
  verbose=verbose,
42
44
  )
43
45
  else:
@@ -65,8 +67,8 @@ def outloud_azure(
65
67
 
66
68
  # This example requires environment variables named "SPEECH_KEY" and "SPEECH_REGION"
67
69
  speech_config = speechsdk.SpeechConfig(
68
- subscription=os.environ.get("SPEECH_KEY"),
69
- region=os.environ.get("SPEECH_REGION"),
70
+ subscription=get_env_var("SPEECH_KEY"),
71
+ region=get_env_var("SPEECH_REGION"),
70
72
  )
71
73
  # https://learn.microsoft.com/en-us/answers/questions/693848/can-azure-text-to-speech-support-more-audio-files.html
72
74
  speech_config.set_speech_synthesis_output_format(
@@ -78,7 +80,7 @@ def outloud_azure(
78
80
 
79
81
  # The language of the voice that speaks.
80
82
  # speech_config.speech_synthesis_voice_name = "en-US-JennyNeural"
81
- speech_config.speech_synthesis_voice_name = bot_name
83
+ speech_config.speech_synthesis_voice_name = bot_name # type: ignore
82
84
 
83
85
  speech_synthesizer = speechsdk.SpeechSynthesizer(
84
86
  speech_config=speech_config, audio_config=audio_config
@@ -100,13 +102,13 @@ def outloud_azure(
100
102
  speech_synthesis_result = speech_synthesizer.speak_ssml_async(ssml).get()
101
103
 
102
104
  if (
103
- speech_synthesis_result.reason
105
+ speech_synthesis_result.reason # type: ignore
104
106
  == speechsdk.ResultReason.SynthesizingAudioCompleted
105
107
  ):
106
108
  if verbose > 0:
107
109
  print("Speech synthesized for text [{}]".format(text))
108
- elif speech_synthesis_result.reason == speechsdk.ResultReason.Canceled:
109
- cancellation_details = speech_synthesis_result.cancellation_details
110
+ elif speech_synthesis_result.reason == speechsdk.ResultReason.Canceled: # type: ignore
111
+ cancellation_details = speech_synthesis_result.cancellation_details # type: ignore
110
112
  print("Speech synthesis canceled: {}".format(cancellation_details.reason))
111
113
  if cancellation_details.reason == speechsdk.CancellationReason.Error:
112
114
  if cancellation_details.error_details:
@@ -193,7 +195,7 @@ def outloud_elevenlabs(
193
195
  from elevenlabs.client import ElevenLabs
194
196
 
195
197
  if api_key is None:
196
- api_key = os.environ.get("ELEVENLABS_API_KEY")
198
+ api_key = get_env_var("ELEVENLABS_API_KEY")
197
199
  # i should figure out a better way to handle playing an mp3 file
198
200
  # assert (
199
201
  # not mp3_filen and should_play
@@ -2,9 +2,12 @@ from gjdutils.functions import variable_from_caller
2
2
 
3
3
 
4
4
  def vprint(min_verbosity: int, msg: str, /, **kwargs):
5
- """Print if the caller's verbosity level is >= min_verbosity.
5
+ """Print if the caller's verbosity level is >= min_verbosity, e.g.
6
6
 
7
- This function looks for a 'verbose' variable in the caller's scope.
7
+ verbose = 2
8
+ vprint(1, "Hello, world!") # prints because verbose >= 1
9
+
10
+ This function looks for a `verbose` variable in the caller's scope.
8
11
 
9
12
  Args:
10
13
  min_verbosity: Minimum verbosity level required to print (positional-only)
@@ -4,16 +4,30 @@ from pathlib import Path
4
4
  import urllib.request
5
5
  import urllib.error
6
6
  import shutil
7
+ import tomllib
7
8
  from typing import Literal
8
9
  from rich.console import Console
9
10
  from rich.progress import track
10
11
  from packaging.version import Version
12
+ from importlib.metadata import metadata
11
13
 
12
14
  from gjdutils.cmd import run_cmd
13
- from gjdutils import __version__
14
15
 
15
16
  console = Console()
16
17
 
18
+
19
+ def get_version() -> str:
20
+ """Get package version from pyproject.toml.
21
+
22
+ We read directly from pyproject.toml rather than using Python's packaging machinery
23
+ because we've had issues with the correct version being accessed in deployment/checking
24
+ machinery that creates virtualenvs. This provides a more reliable way to access the
25
+ true version from the source."""
26
+ with open("pyproject.toml", "rb") as f:
27
+ data = tomllib.load(f)
28
+ return data["project"]["version"]
29
+
30
+
17
31
  # Type for PyPI environment
18
32
  PyPIEnv = Literal["test", "prod"]
19
33
 
@@ -25,26 +39,27 @@ def verify_installation(python_path: Path):
25
39
  before_msg="Verify package installation by importing and checking version...",
26
40
  fatal_msg="Failed to import gjdutils",
27
41
  )
42
+ expected_version = get_version()
28
43
  assert (
29
- installed_version == __version__
30
- ), f"Installed version {installed_version} does not match expected version {__version__}"
44
+ installed_version == expected_version
45
+ ), f"Installed version {installed_version} does not match expected version {expected_version}"
31
46
  console.print(f"gjdutils version: {installed_version}")
32
47
  return installed_version
33
48
 
34
49
 
35
50
  def check_install_optional_features(python_path: Path, *, from_test_pypi: bool = False):
36
51
  """Test installation of optional feature sets."""
37
- features = [
38
- "audio_lang",
39
- "dt",
40
- "llm",
41
- "html_web",
42
- ]
52
+ # Get optional dependency groups from package metadata
53
+ pkg_metadata = metadata("gjdutils")
54
+ # Parse the provides-extra field to get optional dependency groups
55
+ # get_all() returns None if the field doesn't exist
56
+ extra_features = pkg_metadata.get_all("Provides-Extra") or []
57
+ features = [group for group in extra_features if group not in ["dev", "all_no_dev"]]
43
58
 
44
59
  for feature in track(features, description="Installing features"):
45
60
  console.print(f"\nTesting feature set: {feature}", style="yellow")
46
61
  if from_test_pypi:
47
- cmd = f"{python_path} -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ gjdutils"
62
+ cmd = f"{python_path} -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ gjdutils[{feature}]"
48
63
  else:
49
64
  cmd = f"{python_path} -m pip install '.[{feature}]'"
50
65
  run_cmd(
@@ -1,6 +1,6 @@
1
1
  import sys
2
2
 
3
- from gjdutils.env import get_env_var
3
+ # keep this, because it makes sense for the user to be able to import this from here
4
4
  from gjdutils.pytest_utils import in_pytest
5
5
 
6
6
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  from pathlib import Path
4
4
  import shutil
5
- import subprocess
6
5
  import sys
7
6
  import venv
8
7
  from contextlib import contextmanager
@@ -121,7 +121,7 @@ def jinja_render(
121
121
 
122
122
  Will raise an error if CONTEXT is missing any variables.
123
123
  """
124
- from jinja2 import Environment, FileSystemLoader, StrictUndefined, Template
124
+ from jinja2 import Environment, FileSystemLoader, StrictUndefined
125
125
 
126
126
  loader = None if filesystem_loader is None else FileSystemLoader(filesystem_loader)
127
127
  env = Environment(loader=loader, undefined=StrictUndefined)
@@ -200,11 +200,18 @@ def append_fullstop(s: str):
200
200
 
201
201
 
202
202
  def str_from_num(x):
203
+ try:
204
+ import numpy as np
205
+
206
+ bools = (bool, np.bool_)
207
+ except ImportError:
208
+ bools = (bool,)
209
+
203
210
  # todo, find a better way of doing this, e.g.
204
211
  # if isinstance(Iterable), recur. otherwise try str(x)
205
212
  if x is None:
206
213
  return str(x)
207
- elif isinstance(x, (bool, np.bool_)):
214
+ elif isinstance(x, bools):
208
215
  return str(x)
209
216
  elif isinstance(x, str):
210
217
  return x
@@ -1,3 +1,4 @@
1
+ # keep this, because it makes sense for the user to be able to import from here
1
2
  from inspect import isfunction
2
3
 
3
4
  # these are occasionally useful for if statements,
@@ -32,6 +33,6 @@ def isiterable(x):
32
33
  """
33
34
  from http://stackoverflow.com/questions/1952464/in-python-how-do-i-determine-if-a-variable-is-iterable
34
35
  """
35
- import collections
36
+ import collections.abc
36
37
 
37
- return isinstance(x, collections.Iterable)
38
+ return isinstance(x, collections.abc.Iterable)
@@ -0,0 +1,18 @@
1
+ from pytest import approx, raises
2
+
3
+ from gjdutils.num import discretise
4
+
5
+
6
+ def test_discretise():
7
+ assert discretise(0.00, increment=0.05) == approx(0.00)
8
+ assert discretise(0.01, increment=0.05) == approx(0.00)
9
+ assert discretise(0.06, increment=0.05) == approx(0.05)
10
+ assert discretise(0.99, increment=0.05) == approx(0.95)
11
+ assert discretise(1.00, increment=0.05) == approx(1.00)
12
+ # check values outside the range
13
+ with raises(Exception):
14
+ discretise(5.00, increment=0.05)
15
+ with raises(Exception):
16
+ discretise(-1.00, increment=0.05)
17
+ assert discretise(-1.00, increment=0.05, enforce_range=False) == approx(0.00)
18
+ assert discretise(5.00, increment=0.05, enforce_range=False) == approx(1.00)
@@ -1,5 +1,5 @@
1
1
  import pytest
2
- from gjdutils.vprint import vprint
2
+ from gjdutils.print_utils import vprint
3
3
 
4
4
 
5
5
  def test_vprint_basic():
@@ -1 +0,0 @@
1
- __version__ = "0.3.0"
@@ -1,12 +0,0 @@
1
- import os
2
- import sys
3
-
4
- # Add the root directory to Python path to import __VERSION__
5
- root_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
6
- if root_dir not in sys.path:
7
- sys.path.append(root_dir)
8
-
9
- from __VERSION__ import __version__
10
-
11
- # Export version at package level
12
- __version__ = __version__
@@ -1,34 +0,0 @@
1
- # This dict is always sorted by `sortfunc` when you iterate over it.
2
- #
3
- # For example, if you have a dict of str -> Nodes, and you want to iterate over them in
4
- # order of their `idx` attribute, you can do:
5
- #
6
- # d = SortedDict[str,Node](lambda node: node.idx)
7
- # [insert, remove, Nodes]
8
- # d.values() # this will always be sorted by idx
9
- #
10
- # based on code from ChatGPT
11
-
12
- from typing import Any, Callable, Dict, Generic, TypeVar
13
-
14
-
15
- # Define type variables for keys and values
16
- K = TypeVar("K")
17
- V = TypeVar("V")
18
-
19
-
20
- class SortedDict(Dict[K, V], Generic[K, V]):
21
- def __init__(self, sortfunc: Callable[[V], Any], *args, **kwargs):
22
- super().__init__(*args, **kwargs)
23
- self.sortfunc = sortfunc
24
- self._reorder()
25
-
26
- def __setitem__(self, key, value):
27
- super().__setitem__(key, value)
28
- self._reorder()
29
-
30
- def _reorder(self):
31
- sorted_items = sorted(self.items(), key=lambda item: self.sortfunc(item[1]))
32
- super().clear()
33
- for key, value in sorted_items:
34
- super().__setitem__(key, value)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes