sciris 3.5.1__tar.gz → 3.5.2__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.
- {sciris-3.5.1 → sciris-3.5.2}/CHANGELOG.md +6 -0
- {sciris-3.5.1/sciris.egg-info → sciris-3.5.2}/PKG-INFO +1 -1
- {sciris-3.5.1 → sciris-3.5.2}/sciris/sc_utils.py +56 -16
- {sciris-3.5.1 → sciris-3.5.2}/sciris/sc_version.py +2 -2
- {sciris-3.5.1 → sciris-3.5.2/sciris.egg-info}/PKG-INFO +1 -1
- {sciris-3.5.1 → sciris-3.5.2}/sciris.egg-info/SOURCES.txt +0 -1
- {sciris-3.5.1 → sciris-3.5.2}/tests/pytest.ini +1 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/test_utils.py +22 -2
- sciris-3.5.1/sciris_issues.md +0 -21
- {sciris-3.5.1 → sciris-3.5.2}/.claude-plugin/marketplace.json +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/.github/workflows/publish_docs.yaml +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/.github/workflows/pypi_release.yaml +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/.github/workflows/schedule_tests.yaml +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/.github/workflows/test_docs.yaml +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/.github/workflows/test_downstream.yaml +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/.github/workflows/test_sciris.yaml +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/.gitignore +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/.pylintrc +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/.pytest_cache/README.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/CLAUDE.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/CODE_OF_CONDUCT.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/CONTRIBUTING.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/LICENSE +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/MANIFEST.in +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/README.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/STYLE_GUIDE.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/claude_plugin/.claude-plugin/plugin.json +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/claude_plugin/README.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/claude_plugin/skills/sciris-advanced/SKILL.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/claude_plugin/skills/sciris-arrays/SKILL.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/claude_plugin/skills/sciris-dates/SKILL.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/claude_plugin/skills/sciris-dicts/SKILL.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/claude_plugin/skills/sciris-files/SKILL.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/claude_plugin/skills/sciris-intro/SKILL.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/claude_plugin/skills/sciris-parallel/SKILL.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/claude_plugin/skills/sciris-plotting/SKILL.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/claude_plugin/skills/sciris-printing/SKILL.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/claude_plugin/skills/sciris-utils/SKILL.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/context7.json +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/.gitignore +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/404.qmd +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/CNAME +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/README.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/_extensions/machow/interlinks/objects.txt +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/_quarto.yml +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/_site/llms-full.txt +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/_site/llms.txt +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/_site/robots.txt +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/_variables.yml +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/api/index.qmd +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/assets/favicon.ico +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/assets/sciris-logo-dark.png +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/assets/sciris-logo-grey.png +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/assets/sciris-logo-large.png +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/assets/sciris-logo.png +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/assets/sciris-showcase-code.png +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/assets/sciris-showcase-output.png +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/assets/styles-dark.scss +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/assets/styles-light.scss +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/assets/styles.css +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/check_notebooks.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/clean_all +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/clean_outputs.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/conduct.qmd +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/contributing.qmd +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/index.qmd +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/make_api.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/overview.qmd +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/preview +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/publish +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/quarto_utils.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/render +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/requirements.txt +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/sciris-citation.bib +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/style_guide.qmd +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/.jupyter_cache/__version__.txt +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/README.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/convert_notebooks +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/dancing_lines.gif +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/img-advanced.gif +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/img-arrays.png +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/img-dates.png +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/img-dicts.png +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/img-files.png +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/img-intro.png +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/img-parallel.png +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/img-plotting.png +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/img-printing.png +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/img-utils.png +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/sim1/sim.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/sim2/sim.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/tut_advanced.qmd +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/tut_arrays.qmd +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/tut_dates.qmd +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/tut_dicts.qmd +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/tut_files.qmd +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/tut_intro.qmd +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/tut_parallel.qmd +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/tut_plotting.qmd +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/tut_printing.qmd +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials/tut_utils.qmd +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/tutorials.qmd +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/docs/whatsnew.qmd +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/pyproject.toml +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/README.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/__init__.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/_extras/README.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/_extras/__init__.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/_extras/ansicolors.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/fancy.mplstyle +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/sc_asd.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/sc_colors.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/sc_dataframe.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/sc_datetime.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/sc_fileio.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/sc_math.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/sc_nested.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/sc_odict.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/sc_parallel.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/sc_plotting.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/sc_printing.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/sc_profiling.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/sc_settings.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/sc_versioning.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris/simple.mplstyle +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris.egg-info/dependency_links.txt +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris.egg-info/requires.txt +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/sciris.egg-info/top_level.txt +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/setup.cfg +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/.coveragerc +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/.pytest_cache/README.md +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/README.rst +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/check_coverage +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/check_style +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/files/archive_2023-04-19.zip +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/files/deadclass.obj +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/files/deadclass.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/files/exampledata.xlsx +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/files/examplefont.ttf +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/files/make_archive.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/files/make_deadclass.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/files/pickle_2021-01-01.obj +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/files/pickle_2022-01-01.obj +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/regression/README.rst +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/regression/run_downstream +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/regression/run_regression +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/regression/sciris2021-01-01.yml +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/regression/sciris2022-01-01.yml +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/regression/sciris2023-01-01.yml +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/regression/setup_envs +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/requirements.txt +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/run_tests +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/sc_test_utils.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/showcase_sciris.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/showcase_vanilla.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/test_api.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/test_asd.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/test_colors.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/test_dataframe.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/test_datetime.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/test_fileio.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/test_math.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/test_nested.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/test_odict.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/test_parallel.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/test_plotting.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/test_printing.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/test_profiling.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/test_settings.py +0 -0
- {sciris-3.5.1 → sciris-3.5.2}/tests/test_versioning.py +0 -0
|
@@ -4,6 +4,12 @@ All major updates to Sciris are documented here.
|
|
|
4
4
|
|
|
5
5
|
By import convention, components of the Sciris library are listed beginning with `sc.`, e.g. `sc.odict()`.
|
|
6
6
|
|
|
7
|
+
## Version 3.5.2 (2026-10-16)
|
|
8
|
+
|
|
9
|
+
1. `sc.urlopen()` and `sc.download()` have a new `timeout` argument (default 30 seconds; `None` waits forever, as before), and a new `progress` argument to show a progress bar.
|
|
10
|
+
1. `sc.urlopen()` now adds the `prefix` to any URL without a scheme (previously, e.g. `httpbin.org` was left alone because it starts with "http"), accepts a string or bytes for `data`, and closes the connection after reading.
|
|
11
|
+
1. `sc.urlopen()` and `sc.download()` no longer include query strings in default filenames (e.g. `data?a=1` is now saved as `data`).
|
|
12
|
+
|
|
7
13
|
## Version 3.5.1 (2026-10-15)
|
|
8
14
|
|
|
9
15
|
1. `sc.iterobj()` and `sc.search()` are about twice as fast; `sc.iterobj()` no longer descends into classes or NumPy dtypes stored as attributes (e.g. `self.dtype = np.int64`), since their attributes belong to the class rather than the object. To restore the previous behavior, pass `atomic=sc.sc_nested.atomic_with_tuple`.
|
|
@@ -756,7 +756,7 @@ def asciify(string, form='NFKD', encoding='ascii', errors='ignore', **kwargs):
|
|
|
756
756
|
__all__ += ['urlopen', 'wget', 'download', 'htmlify']
|
|
757
757
|
|
|
758
758
|
def urlopen(url, filename=None, save=None, headers=None, params=None, data=None,
|
|
759
|
-
prefix='http', convert=True, die=False, response='text', verbose=False):
|
|
759
|
+
prefix='http', convert=True, die=False, response='text', timeout=30, progress=False, verbose=False):
|
|
760
760
|
"""
|
|
761
761
|
Download a single URL.
|
|
762
762
|
|
|
@@ -769,11 +769,13 @@ def urlopen(url, filename=None, save=None, headers=None, params=None, data=None,
|
|
|
769
769
|
save (bool): if supplied instead of `filename`, then use the default filename
|
|
770
770
|
headers (dict): a dictionary of headers to pass
|
|
771
771
|
params (dict): a dictionary of parameters to pass to the GET request
|
|
772
|
-
data (dict) a dictionary of parameters to pass to a POST request
|
|
773
|
-
prefix (str): the
|
|
772
|
+
data (dict/str/bytes): a dictionary of parameters to pass to a POST request (or the already-encoded body)
|
|
773
|
+
prefix (str): the scheme to add if the URL doesn't have one (e.g. 'http' turns 'wikipedia.org' into 'http://wikipedia.org')
|
|
774
774
|
convert (bool): whether to convert from bytes to string
|
|
775
775
|
die (bool): whether to raise an exception if converting to text failed
|
|
776
776
|
response (str): what to return: 'text' (default), 'json' (dictionary version of the data), 'status' (the HTTP status), or 'full' (the full response object)
|
|
777
|
+
timeout (float): how many seconds to wait for the server to respond before raising an error (None to wait forever); this limits each connection or read attempt, not the whole download
|
|
778
|
+
progress (bool): whether to show a progress bar while downloading (a running total if the server doesn't give the size)
|
|
777
779
|
verbose (bool): whether to print progress
|
|
778
780
|
|
|
779
781
|
**Examples**:
|
|
@@ -783,12 +785,15 @@ def urlopen(url, filename=None, save=None, headers=None, params=None, data=None,
|
|
|
783
785
|
sc.urlopen('http://wikipedia.org', filename='wikipedia.html') # Save to file wikipedia.html
|
|
784
786
|
sc.urlopen('https://wikipedia.org', save=True, headers={'User-Agent':'Custom agent'}) # Save to the default filename (here, wikipedia.org), with headers
|
|
785
787
|
sc.urlopen('wikipedia.org', response='status') # Only return the HTTP status of the site
|
|
788
|
+
sc.urlopen('wikipedia.org', timeout=5) # Give up if the server doesn't respond within 5 seconds
|
|
789
|
+
sc.urlopen('https://example.com/bigfile.zip', save=True, progress=True) # Show a progress bar
|
|
786
790
|
```
|
|
787
791
|
|
|
788
792
|
- *New in version 2.0.0:* renamed from `wget` to `urlopen`; new arguments
|
|
789
793
|
- *New in version 2.0.1:* creates folders by default if they do not exist
|
|
790
794
|
- *New in version 2.0.4:* "prefix" argument, e.g. prepend "http://" if not present
|
|
791
795
|
- *New in version 3.1.4:* renamed "return_response" to "response"; additional options
|
|
796
|
+
- *New in version 3.5.2:* "timeout" and "progress" arguments; "data" can be a string or bytes; the prefix is added to any URL without a scheme (e.g. "httpbin.org")
|
|
792
797
|
"""
|
|
793
798
|
from urllib import request as ur # Need to import these directly, not via urllib
|
|
794
799
|
from urllib import parse as up
|
|
@@ -805,24 +810,29 @@ def urlopen(url, filename=None, save=None, headers=None, params=None, data=None,
|
|
|
805
810
|
|
|
806
811
|
# Handle parameters and data
|
|
807
812
|
full_url = url
|
|
808
|
-
if prefix is not None:
|
|
809
|
-
|
|
810
|
-
full_url = prefix + '://' + full_url # Leaves https alone, but adds http:// otherwise
|
|
813
|
+
if prefix is not None and '://' not in full_url:
|
|
814
|
+
full_url = prefix + '://' + full_url # Leaves e.g. https:// alone, but adds http:// otherwise
|
|
811
815
|
if params is not None:
|
|
812
816
|
full_url += ('&' if '?' in full_url else '?') + up.urlencode(params) # Append to an existing query string if present
|
|
813
|
-
if data
|
|
817
|
+
if isinstance(data, str):
|
|
818
|
+
data = data.encode('utf-8')
|
|
819
|
+
elif data is not None and not isinstance(data, bytes):
|
|
814
820
|
data = up.urlencode(data).encode(encoding='utf-8', errors='ignore')
|
|
815
821
|
|
|
816
822
|
if verbose: print(f'Downloading {url}...')
|
|
817
823
|
request = ur.Request(full_url, headers=headers, data=data)
|
|
818
824
|
try:
|
|
819
|
-
resp = ur.urlopen(request) # Actually open the URL
|
|
825
|
+
resp = ur.urlopen(request, timeout=timeout) # Actually open the URL
|
|
820
826
|
except ue.HTTPError as E: # e.g. 404
|
|
821
827
|
if response == 'status':
|
|
828
|
+
E.close()
|
|
822
829
|
return E.code # Return the error status rather than raising
|
|
823
830
|
raise
|
|
831
|
+
if response != 'full': # Read what we need now, then close the connection
|
|
832
|
+
with resp:
|
|
833
|
+
body = _readurl(resp, progress) if response in ['text', 'json'] else None
|
|
824
834
|
if response in ['text', 'json']:
|
|
825
|
-
output =
|
|
835
|
+
output = body
|
|
826
836
|
if response == 'json':
|
|
827
837
|
try:
|
|
828
838
|
output = sc.loadjson(string=output)
|
|
@@ -851,7 +861,7 @@ def urlopen(url, filename=None, save=None, headers=None, params=None, data=None,
|
|
|
851
861
|
if filename is None and save:
|
|
852
862
|
filename = resp.headers.get_filename() # From the "Content-Disposition" header, if present
|
|
853
863
|
if not filename:
|
|
854
|
-
filename = url
|
|
864
|
+
filename = _url_filename(url)
|
|
855
865
|
|
|
856
866
|
if filename is not None and save is not False:
|
|
857
867
|
if verbose: print(f'Saving to {filename}...')
|
|
@@ -875,7 +885,32 @@ def urlopen(url, filename=None, save=None, headers=None, params=None, data=None,
|
|
|
875
885
|
wget = urlopen
|
|
876
886
|
|
|
877
887
|
|
|
878
|
-
def
|
|
888
|
+
def _readurl(resp, progress=False, chunksize=2**16):
|
|
889
|
+
""" Read an HTTP response, optionally with a progress bar """
|
|
890
|
+
if not progress:
|
|
891
|
+
return resp.read()
|
|
892
|
+
total = resp.length # From the Content-Length header; None if the server doesn't give it
|
|
893
|
+
chunks = []
|
|
894
|
+
done = 0
|
|
895
|
+
while chunk := resp.read(chunksize):
|
|
896
|
+
chunks.append(chunk)
|
|
897
|
+
done += len(chunk)
|
|
898
|
+
label = f'Downloaded {sc.humanize_bytes(done):>10}' # Fixed width, so the line is fully overwritten
|
|
899
|
+
if total:
|
|
900
|
+
sc.progressbar(done, total, label=label, flush=True) # Ends the line when done == total
|
|
901
|
+
else:
|
|
902
|
+
print(f'\r{label}', end='', flush=True)
|
|
903
|
+
if done != total:
|
|
904
|
+
print() # End the line
|
|
905
|
+
return b''.join(chunks)
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
def _url_filename(url):
|
|
909
|
+
""" Turn e.g. 'http://mysite.com/tree/index.html?a=1' into 'index.html' """
|
|
910
|
+
return url.split('?')[0].split('#')[0].rstrip('/').split('/')[-1]
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def download(url, *args, filename=None, save=True, parallel=True, die=True, progress=False, verbose=True, **kwargs):
|
|
879
914
|
"""
|
|
880
915
|
Download one or more URLs in parallel and return output or save them to disk.
|
|
881
916
|
|
|
@@ -888,8 +923,9 @@ def download(url, *args, filename=None, save=True, parallel=True, die=True, verb
|
|
|
888
923
|
save (bool): if supplied instead of `filename`, then use the default filename
|
|
889
924
|
parallel (bool): whether to download multiple URLs in parallel
|
|
890
925
|
die (bool): whether to raise an exception if a URL can't be retrieved (default true)
|
|
926
|
+
progress (bool): whether to show a progress bar (of bytes for a single URL, or of URLs completed for several)
|
|
891
927
|
verbose (bool): whether to print progress (if verbose=2, print extra detail on each downloaded URL)
|
|
892
|
-
**kwargs (dict): passed to `sc.urlopen()`
|
|
928
|
+
**kwargs (dict): passed to `sc.urlopen()` (e.g. `timeout`)
|
|
893
929
|
|
|
894
930
|
**Examples**:
|
|
895
931
|
|
|
@@ -899,12 +935,14 @@ def download(url, *args, filename=None, save=True, parallel=True, die=True, verb
|
|
|
899
935
|
sc.download({'sciris.html':'http://sciris.org', 'covasim.html':'http://covasim.org'}) # Download two and save to disk
|
|
900
936
|
sc.download(['http://sciris.org', 'http://covasim.org'], filename=['sciris.html', 'covasim.html']) # Ditto
|
|
901
937
|
data = sc.download(dict(sciris='http://sciris.org', covasim='http://covasim.org'), save=False) # Download and store in memory
|
|
938
|
+
sc.download('https://example.com/bigfile.zip', progress=True, timeout=10) # Show a progress bar, and give up if the server stops responding for 10 seconds
|
|
902
939
|
```
|
|
903
940
|
|
|
904
941
|
- *New in version 2.0.0.*
|
|
905
942
|
- *New in version 3.0.0:* "die" argument
|
|
906
943
|
- *New in version 3.1.1:* default order switched from URL:filename to filename:URL pairs
|
|
907
944
|
- *New in version 3.1.3:* output as objdict instead of odict
|
|
945
|
+
- *New in version 3.5.2:* "progress" argument
|
|
908
946
|
"""
|
|
909
947
|
T = sc.timer()
|
|
910
948
|
|
|
@@ -927,7 +965,7 @@ def download(url, *args, filename=None, save=True, parallel=True, die=True, verb
|
|
|
927
965
|
else: # Otherwise, turn the URL into a filename
|
|
928
966
|
keys = []
|
|
929
967
|
for url in urls: # Turn e.g. 'http://mysite.com/tree/index.html' into 'index.html', or 'index.html1' if it's a duplicate
|
|
930
|
-
keys.append(uniquename(url
|
|
968
|
+
keys.append(uniquename(_url_filename(url), keys))
|
|
931
969
|
elif n_keys != n_urls: # pragma: no cover
|
|
932
970
|
errormsg = f'Cannot process {n_urls} URLs and {n_keys} filenames'
|
|
933
971
|
raise ValueError(errormsg)
|
|
@@ -938,12 +976,12 @@ def download(url, *args, filename=None, save=True, parallel=True, die=True, verb
|
|
|
938
976
|
# Get results in parallel
|
|
939
977
|
wget_verbose = (verbose>1) or (verbose and n_urls == 1) # By default, don't print progress on each download
|
|
940
978
|
iterkwargs = dict(url=urls, filename=keys)
|
|
941
|
-
func_kwargs = mergedicts(dict(save=save, verbose=wget_verbose), kwargs)
|
|
979
|
+
func_kwargs = mergedicts(dict(save=save, verbose=wget_verbose, progress=progress and n_urls == 1), kwargs) # For several URLs, show progress by URL instead of by byte
|
|
942
980
|
if n_urls > 1 and parallel:
|
|
943
|
-
outputs = sc.parallelize(urlopen, iterkwargs=iterkwargs, kwargs=func_kwargs, parallelizer='thread', die=die)
|
|
981
|
+
outputs = sc.parallelize(urlopen, iterkwargs=iterkwargs, kwargs=func_kwargs, parallelizer='thread', progress=progress, die=die)
|
|
944
982
|
else:
|
|
945
983
|
outputs = []
|
|
946
|
-
for key,url in zip(keys, urls):
|
|
984
|
+
for i,(key,url) in enumerate(zip(keys, urls)):
|
|
947
985
|
try:
|
|
948
986
|
output = urlopen(url=url, filename=key, **func_kwargs)
|
|
949
987
|
except Exception as E:
|
|
@@ -955,6 +993,8 @@ def download(url, *args, filename=None, save=True, parallel=True, die=True, verb
|
|
|
955
993
|
output = E
|
|
956
994
|
|
|
957
995
|
outputs.append(output)
|
|
996
|
+
if progress and n_urls > 1:
|
|
997
|
+
sc.progressbar(i+1, n_urls, label=f'URL {i+1}/{n_urls}', flush=True)
|
|
958
998
|
|
|
959
999
|
# If we're returning the data rather than saving the files, convert to an odict
|
|
960
1000
|
if not save:
|
|
@@ -4,6 +4,6 @@ Version and license information.
|
|
|
4
4
|
|
|
5
5
|
__all__ = ['__version__', '__versiondate__', '__license__']
|
|
6
6
|
|
|
7
|
-
__version__ = '3.5.
|
|
8
|
-
__versiondate__ = '2026-10-
|
|
7
|
+
__version__ = '3.5.2'
|
|
8
|
+
__versiondate__ = '2026-10-16'
|
|
9
9
|
__license__ = f'Sciris {__version__} ({__versiondate__}) – © 2014-2026 by the Sciris Development Team'
|
|
@@ -91,11 +91,17 @@ def test_urlopen_local():
|
|
|
91
91
|
print('\nTesting urlopen with a local server')
|
|
92
92
|
|
|
93
93
|
class Handler(BaseHTTPRequestHandler):
|
|
94
|
+
def log_message(self, *args):
|
|
95
|
+
pass # Don't print each request
|
|
96
|
+
|
|
94
97
|
def do_GET(self):
|
|
95
98
|
if self.path == '/missing':
|
|
96
99
|
self.send_response(404)
|
|
97
100
|
self.end_headers()
|
|
98
101
|
return
|
|
102
|
+
if self.path == '/slow':
|
|
103
|
+
sc.timedsleep(1)
|
|
104
|
+
return # The client has timed out by now, so don't reply
|
|
99
105
|
self.send_response(200)
|
|
100
106
|
if self.path == '/latin1':
|
|
101
107
|
self.send_header('Content-Type', 'text/plain; charset=iso-8859-1')
|
|
@@ -103,6 +109,14 @@ def test_urlopen_local():
|
|
|
103
109
|
else:
|
|
104
110
|
self.send_header('Content-Disposition', 'attachment; filename="temp_report.csv"')
|
|
105
111
|
body = self.path.encode()
|
|
112
|
+
if self.path == '/sized':
|
|
113
|
+
self.send_header('Content-Length', str(len(body)))
|
|
114
|
+
self.end_headers()
|
|
115
|
+
self.wfile.write(body)
|
|
116
|
+
|
|
117
|
+
def do_POST(self):
|
|
118
|
+
body = self.rfile.read(int(self.headers['Content-Length']))
|
|
119
|
+
self.send_response(200)
|
|
106
120
|
self.end_headers()
|
|
107
121
|
self.wfile.write(body)
|
|
108
122
|
|
|
@@ -113,8 +127,14 @@ def test_urlopen_local():
|
|
|
113
127
|
assert sc.urlopen(url + '/missing', response='status') == 404
|
|
114
128
|
assert sc.urlopen(url + '/api?a=1', params=dict(b=2)) == '/api?a=1&b=2'
|
|
115
129
|
assert sc.urlopen(url + '/latin1') == 'café'
|
|
116
|
-
|
|
117
|
-
assert
|
|
130
|
+
assert sc.urlopen(url + '/post', data=b'raw') == 'raw'
|
|
131
|
+
assert sc.urlopen(url + '/sized', progress=True) == '/sized' # With Content-Length
|
|
132
|
+
assert sc.urlopen(url + '/unsized', progress=True) == '/unsized' # Without
|
|
133
|
+
with pytest.raises(TimeoutError):
|
|
134
|
+
sc.urlopen(url + '/slow', timeout=0.1)
|
|
135
|
+
data = sc.download(f'http://{url}/v1/data', f'http://{url}/v2/data?a=1', save=False, progress=True)
|
|
136
|
+
assert list(data.keys()) == ['data', 'data1'] # Query strings are not part of the filename
|
|
137
|
+
assert list(data.values()) == ['/v1/data', '/v2/data?a=1'] # Duplicate filenames are not merged
|
|
118
138
|
filename = sc.urlopen(url + '/report', save=True) # Filename from the Content-Disposition header
|
|
119
139
|
assert sc.path(filename).name == 'temp_report.csv'
|
|
120
140
|
sc.rmpath(filename)
|
sciris-3.5.1/sciris_issues.md
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# Sciris issues
|
|
2
|
-
|
|
3
|
-
Found while speeding up how Starsim v4 builds a sim (Sciris as installed in the `v4` env). Nothing here has been changed in Sciris.
|
|
4
|
-
|
|
5
|
-
## `sc.search()` builds the whole tree before matching
|
|
6
|
-
|
|
7
|
-
`sc.search()` calls `sc.iterobj()` with the default function, which stores every object it visits in `output`, keyed by `tuple(trace)` (an `sc.objdict` `__setitem__` per object), and only then filters that tree for matches. For a search by type, almost all of that work is thrown away. Building a Starsim sim does five such searches (for distributions and time parameters), and they cost about 3 µs per object visited. That was most of the time to build a small sim (about 8 of 13 ms before the changes in Starsim, and about 4 of 7 ms after them).
|
|
8
|
-
|
|
9
|
-
Suggested fix: give `iterobj()` a mode in which the function only records matches (or let `search()` pass a function that keeps only the objects that match, and not store the rest), so a search costs only the walk.
|
|
10
|
-
|
|
11
|
-
## `IterObj` deep-copies the `skip` dict on every call
|
|
12
|
-
|
|
13
|
-
`IterObj.__init__` does `skip = sc.dcp(skip)` when `skip` is a dict. Starsim passes a list of a few hundred object IDs in `skip['ids']`, so each search starts by deep-copying it. A shallow copy (or `dict(skip)`, since the values are only read) would do.
|
|
14
|
-
|
|
15
|
-
## `sc.iterobj()` descends into classes and NumPy dtypes
|
|
16
|
-
|
|
17
|
-
With the default `atomic`, a class object (e.g. `np.int64` stored as an attribute `dtype`) is treated as an object and its `__dict__` is walked: 49 extra visits for one such attribute. Classes and `np.dtype` instances could be atomic by default, as strings and arrays are. (Starsim now skips them itself.)
|
|
18
|
-
|
|
19
|
-
## `sc.yeartodate()` is slow
|
|
20
|
-
|
|
21
|
-
About 65 µs a call, most of it in `sc.datedelta()`, which sanitizes its arguments with `sc.dcp()` and goes through `dateutil.relativedelta`. `ss.date.from_year()` called it once per time point, which made building a sim with weekly steps over 30 years take about 70 ms. Starsim now computes the date directly (1 January of the year, plus `round(fraction × days in the year)` days, which gives identical dates on 95,445 test years). The same shortcut in `yeartodate()` itself would help other callers. A vectorized form, for an array of years, would help more.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|