numpyro-forecast 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. numpyro_forecast-0.1.0/.github/workflows/ci.yml +33 -0
  2. numpyro_forecast-0.1.0/.github/workflows/claude-code-review.yml +43 -0
  3. numpyro_forecast-0.1.0/.github/workflows/claude.yml +49 -0
  4. numpyro_forecast-0.1.0/.github/workflows/release.yml +77 -0
  5. numpyro_forecast-0.1.0/.gitignore +222 -0
  6. numpyro_forecast-0.1.0/.pre-commit-config.yaml +29 -0
  7. numpyro_forecast-0.1.0/AGENTS.md +78 -0
  8. numpyro_forecast-0.1.0/CLAUDE.md +1 -0
  9. numpyro_forecast-0.1.0/CONTRIBUTING.md +28 -0
  10. numpyro_forecast-0.1.0/LICENSE +201 -0
  11. numpyro_forecast-0.1.0/Makefile +13 -0
  12. numpyro_forecast-0.1.0/PKG-INFO +266 -0
  13. numpyro_forecast-0.1.0/README.md +213 -0
  14. numpyro_forecast-0.1.0/examples/forecasting_univariate.ipynb +826 -0
  15. numpyro_forecast-0.1.0/examples/hierarchical_forecasting_1.ipynb +710 -0
  16. numpyro_forecast-0.1.0/examples/hierarchical_forecasting_2.ipynb +817 -0
  17. numpyro_forecast-0.1.0/numpyro_forecast/__init__.py +45 -0
  18. numpyro_forecast-0.1.0/numpyro_forecast/datasets.py +104 -0
  19. numpyro_forecast-0.1.0/numpyro_forecast/evaluate.py +279 -0
  20. numpyro_forecast-0.1.0/numpyro_forecast/forecaster.py +308 -0
  21. numpyro_forecast-0.1.0/numpyro_forecast/functional.py +521 -0
  22. numpyro_forecast-0.1.0/numpyro_forecast/metrics.py +65 -0
  23. numpyro_forecast-0.1.0/numpyro_forecast/py.typed +0 -0
  24. numpyro_forecast-0.1.0/numpyro_forecast/typing.py +40 -0
  25. numpyro_forecast-0.1.0/numpyro_forecast/util.py +225 -0
  26. numpyro_forecast-0.1.0/pyproject.toml +128 -0
  27. numpyro_forecast-0.1.0/tests/conftest.py +95 -0
  28. numpyro_forecast-0.1.0/tests/example_models.py +126 -0
  29. numpyro_forecast-0.1.0/tests/test_datasets.py +23 -0
  30. numpyro_forecast-0.1.0/tests/test_evaluate.py +168 -0
  31. numpyro_forecast-0.1.0/tests/test_examples.py +111 -0
  32. numpyro_forecast-0.1.0/tests/test_forecaster.py +165 -0
  33. numpyro_forecast-0.1.0/tests/test_functional.py +360 -0
  34. numpyro_forecast-0.1.0/tests/test_metrics.py +50 -0
  35. numpyro_forecast-0.1.0/tests/test_package.py +56 -0
  36. numpyro_forecast-0.1.0/tests/test_util.py +126 -0
@@ -0,0 +1,33 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ prek:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v6
14
+ - uses: astral-sh/setup-uv@v6
15
+ - run: make setup
16
+ - name: prek check
17
+ uses: j178/prek-action@v1
18
+ env:
19
+ SKIP: no-commit-to-branch
20
+
21
+ test:
22
+ runs-on: ubuntu-latest
23
+ needs: prek
24
+ strategy:
25
+ matrix:
26
+ python-version: ["3.14"]
27
+ steps:
28
+ - uses: actions/checkout@v6
29
+ - uses: astral-sh/setup-uv@v6
30
+ with:
31
+ python-version: ${{ matrix.python-version }}
32
+ - run: make setup
33
+ - run: make tests
@@ -0,0 +1,43 @@
1
+ name: Claude Code Review
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, ready_for_review, reopened]
6
+ # Optional: Only run on specific file changes
7
+ # paths:
8
+ # - "src/**/*.ts"
9
+ # - "src/**/*.tsx"
10
+ # - "src/**/*.js"
11
+ # - "src/**/*.jsx"
12
+
13
+ jobs:
14
+ claude-review:
15
+ # Optional: Filter by PR author
16
+ # if: |
17
+ # github.event.pull_request.user.login == 'external-contributor' ||
18
+ # github.event.pull_request.user.login == 'new-developer' ||
19
+ # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
20
+
21
+ runs-on: ubuntu-latest
22
+ permissions:
23
+ contents: read
24
+ pull-requests: read
25
+ issues: read
26
+ id-token: write
27
+
28
+ steps:
29
+ - name: Checkout repository
30
+ uses: actions/checkout@v4
31
+ with:
32
+ fetch-depth: 1
33
+
34
+ - name: Run Claude Code Review
35
+ id: claude-review
36
+ uses: anthropics/claude-code-action@v1
37
+ with:
38
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
39
+ plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
40
+ plugins: 'code-review@claude-code-plugins'
41
+ prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
42
+ # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
43
+ # or https://code.claude.com/docs/en/cli-reference for available options
@@ -0,0 +1,49 @@
1
+ name: Claude Code
2
+
3
+ on:
4
+ issue_comment:
5
+ types: [created]
6
+ pull_request_review_comment:
7
+ types: [created]
8
+ issues:
9
+ types: [opened, assigned]
10
+ pull_request_review:
11
+ types: [submitted]
12
+
13
+ jobs:
14
+ claude:
15
+ if: |
16
+ (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
17
+ (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
18
+ (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19
+ (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
20
+ runs-on: ubuntu-latest
21
+ permissions:
22
+ contents: read
23
+ pull-requests: read
24
+ issues: read
25
+ id-token: write
26
+ actions: read # Required for Claude to read CI results on PRs
27
+ steps:
28
+ - name: Checkout repository
29
+ uses: actions/checkout@v4
30
+ with:
31
+ fetch-depth: 1
32
+
33
+ - name: Run Claude Code
34
+ id: claude
35
+ uses: anthropics/claude-code-action@v1
36
+ with:
37
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38
+
39
+ # This is an optional setting that allows Claude to read CI results on PRs
40
+ additional_permissions: |
41
+ actions: read
42
+
43
+ # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
44
+ # prompt: 'Update the pull request description to include a summary of changes.'
45
+
46
+ # Optional: Add claude_args to customize behavior and configuration
47
+ # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
48
+ # or https://code.claude.com/docs/en/cli-reference for available options
49
+ # claude_args: '--allowed-tools Bash(gh pr *)'
@@ -0,0 +1,77 @@
1
+ name: release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build:
9
+ name: build sdist and wheel
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v6
13
+ - uses: astral-sh/setup-uv@v6
14
+ with:
15
+ python-version: "3.12"
16
+ - name: Build sdist and wheel
17
+ run: uv build
18
+ - name: Check the sdist installs, imports, and reports the release version
19
+ run: |
20
+ python -m venv venv-sdist
21
+ venv-sdist/bin/python -m pip install dist/numpyro_forecast-*.tar.gz
22
+ venv-sdist/bin/python -c "import numpyro_forecast as nf; assert nf.__version__ == '${{ github.ref_name }}', nf.__version__; print(nf.__version__)"
23
+ - name: Check the wheel installs, imports, and reports the release version
24
+ run: |
25
+ python -m venv venv-wheel
26
+ venv-wheel/bin/python -m pip install dist/numpyro_forecast-*.whl
27
+ venv-wheel/bin/python -c "import numpyro_forecast as nf; assert nf.__version__ == '${{ github.ref_name }}', nf.__version__; print(nf.__version__)"
28
+ - uses: actions/upload-artifact@v7
29
+ with:
30
+ name: dist
31
+ path: dist/*
32
+
33
+ testpypi:
34
+ name: upload to TestPyPI
35
+ needs: [build]
36
+ runs-on: ubuntu-latest
37
+ permissions:
38
+ id-token: write
39
+ steps:
40
+ - uses: actions/download-artifact@v8
41
+ with:
42
+ name: dist
43
+ path: dist
44
+ - uses: pypa/gh-action-pypi-publish@release/v1
45
+ with:
46
+ skip-existing: true
47
+ repository-url: https://test.pypi.org/legacy/
48
+ - uses: actions/setup-python@v6
49
+ with:
50
+ python-version: "3.12"
51
+ - name: Install from TestPyPI and verify version
52
+ run: |
53
+ python -m venv venv-testpypi
54
+ # TestPyPI's index can lag a few seconds behind the upload, so retry.
55
+ for attempt in 1 2 3 4 5; do
56
+ venv-testpypi/bin/python -m pip install \
57
+ --index-url https://test.pypi.org/simple/ \
58
+ --extra-index-url https://pypi.org/simple \
59
+ "numpyro_forecast==${{ github.ref_name }}" && break
60
+ echo "Not on TestPyPI yet (attempt $attempt); retrying in 15s..."
61
+ sleep 15
62
+ done
63
+ venv-testpypi/bin/python -c "import numpyro_forecast as nf; assert nf.__version__ == '${{ github.ref_name }}', nf.__version__; print(nf.__version__)"
64
+
65
+ publish:
66
+ name: upload to PyPI
67
+ needs: [build, testpypi]
68
+ runs-on: ubuntu-latest
69
+ environment: release
70
+ permissions:
71
+ id-token: write
72
+ steps:
73
+ - uses: actions/download-artifact@v8
74
+ with:
75
+ name: dist
76
+ path: dist
77
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,222 @@
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
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+ # Temporary file for partial code execution
204
+ tempCodeRunnerFile.py
205
+
206
+ # Ruff stuff:
207
+ .ruff_cache/
208
+
209
+ # PyPI configuration file
210
+ .pypirc
211
+
212
+ # Marimo
213
+ marimo/_static/
214
+ marimo/_lsp/
215
+ __marimo__/
216
+
217
+ # Streamlit
218
+ .streamlit/secrets.toml
219
+
220
+ #uv
221
+ .venv/
222
+ uv.lock
@@ -0,0 +1,29 @@
1
+ ci:
2
+ autofix_prs: false
3
+
4
+ repos:
5
+ - repo: https://github.com/astral-sh/ruff-pre-commit
6
+ rev: v0.15.6
7
+ hooks:
8
+ - id: ruff-check
9
+ types_or: [python, pyi, jupyter]
10
+ args: ["--fix", "--output-format=full"]
11
+ - id: ruff-format
12
+ types_or: [python, pyi, jupyter]
13
+ - repo: https://github.com/astral-sh/ty-pre-commit
14
+ rev: v0.0.49
15
+ hooks:
16
+ - id: ty
17
+ - repo: https://github.com/pre-commit/pre-commit-hooks
18
+ rev: v6.0.0
19
+ hooks:
20
+ - id: no-commit-to-branch
21
+ args: [--branch, main]
22
+ stages: [pre-commit, pre-merge-commit, pre-push, manual]
23
+ - id: debug-statements
24
+ - id: trailing-whitespace
25
+ - id: end-of-file-fixer
26
+ - id: check-toml
27
+ - id: check-yaml
28
+ - id: check-added-large-files
29
+ exclude: ^(examples/)
@@ -0,0 +1,78 @@
1
+ # CLAUDE.md
2
+
3
+ Guidance for Claude Code when working in this repository.
4
+
5
+ ## What this is
6
+
7
+ `numpyro_forecast` is a JAX/NumPyro port of Pyro's `pyro.contrib.forecast`
8
+ module. The design context lives in the module docstrings (`forecaster.py`,
9
+ `evaluate.py`, `util.py`) and the example notebooks under `examples/` — read the
10
+ relevant ones before making non-trivial changes.
11
+
12
+ ## Conventions
13
+
14
+ - **Array layout:** time at axis `-2`, observation/event dim at `-1`, batch dims
15
+ to the left (matches Pyro).
16
+ - **Train vs forecast:** a single model handles both. In-sample time latents use
17
+ a fixed site name (`drift`); the forecast horizon uses a separate `_future`
18
+ site so `AutoNormal` never resizes and `Predictive` draws the suffix from the
19
+ prior. The horizon is derived from shapes (`covariates` longer than `data`).
20
+ - **Functional style:** pure model functions, explicit `PRNGKey` threading,
21
+ `jax.lax.scan` for latent levels, no global parameter store.
22
+ - **`rng_key` first:** every JAX/NumPyro function that consumes randomness takes `rng_key: Array` as its first parameter (first after `self` for methods), required and positional (not keyword-only). The one exception is `functools.singledispatch` generics, which must dispatch on their type argument: keep the dispatched generic private (dispatching on its type arg) and expose a thin public wrapper with `rng_key` first (see `draw_posterior` / `_draw_posterior_impl` in `functional.py`).
23
+ - **Integer literals:** write integers with four or more digits using underscore separators so zeros are easy to count: `1_000`, `10_000`, `1_234_567_890` (not `1000`, `1234567890`).
24
+
25
+ ## Hard requirements
26
+
27
+ - Every function (public and private) has complete input and return type hints,
28
+ checked with `ty`.
29
+ - Every public function/class has a NumPy-style docstring (ruff `D`/`DOC`).
30
+ - **jaxtyping:** annotate array shapes as `Float[Array, " time obs"]` with a
31
+ **leading space** in the shape string (per the jaxtyping FAQ this turns ruff's
32
+ `F821` into `F722`, which we ignore globally; `F821` stays active otherwise).
33
+ Do **not** use `from __future__ import annotations` (incompatible with runtime
34
+ type checking).
35
+
36
+ ## Tests
37
+
38
+ For the tests, we use `pytest`.
39
+
40
+ ## Docstrings
41
+
42
+ We use Numpy-like docstrings: https://numpydoc.readthedocs.io/en/latest/format.html
43
+
44
+ ## Writing
45
+
46
+ ### No em-dashes
47
+
48
+ Do not use em-dashes (`—`) in any prose. Use the most natural alternative for the grammatical role the dash was playing: a colon for an explanation or expansion, a comma (or pair of commas) for a parenthetical aside, parentheses for a softer aside, a semicolon for a closely-related independent clause, or a full stop to start a new sentence. Pick the form that reads most cleanly; do not just substitute one punctuation mark mechanically for another.
49
+
50
+ ### No hard line breaks in prose
51
+
52
+ When writing text files (`.txt`, `.md`, `.qmd`, and similar), do **not** wrap prose at a fixed column. Write each paragraph as a single long line and let the editor/renderer handle visual wrapping.
53
+
54
+ - Yes: one line per paragraph, one line per bullet.
55
+ - No: inserting newlines every 80 (or 100, or any other) characters inside a paragraph.
56
+
57
+ Exceptions: code blocks, tables, YAML front matter, and anything where the newline is semantically meaningful (e.g. markdown lists, mermaid diagrams) — keep those formatted normally.
58
+
59
+ ### American English spelling
60
+
61
+ Use American English spelling. Do not use British English spelling.
62
+
63
+ ## Commands
64
+
65
+ See the Makefile for the full workflow.
66
+
67
+ ```bash
68
+ # Install dependencies
69
+ uv sync --all-extras
70
+ # Run pre-commit hooks
71
+ prek run --all-files
72
+ # Lint and format
73
+ uv run ruff check . && uv run ruff format --check .
74
+ # Type check
75
+ uv run ty check numpyro_forecast/
76
+ # Run tests
77
+ uv run pytest
78
+ ```
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,28 @@
1
+ # Contributing
2
+
3
+ Thanks for your interest in `numpyro_forecast`!
4
+
5
+ ## Development setup
6
+
7
+ ```bash
8
+ uv sync --all-extras
9
+ prek install
10
+ ```
11
+
12
+ ## Workflow
13
+
14
+ - **Lint & format:** `uv run ruff check .` and `uv run ruff format .`
15
+ - **Type check:** `uv run ty check numpyro_forecast/`
16
+ - **Tests:** `uv run pytest`
17
+ - **Notebooks:** `uv run pytest --nbmake examples/` (executes the example notebooks)
18
+ - **All hooks:** `prek run --all-files`
19
+
20
+ ## Guidelines
21
+
22
+ - Every function (public and private) must have complete input and return type
23
+ hints. Type checking is enforced with `ty`.
24
+ - Every public function and class must have a NumPy-style docstring.
25
+ - Array shapes are annotated with `jaxtyping` (e.g. `Float[Array, "time obs"]`).
26
+ - Follow the array convention: time at axis `-2`, the observation dim at `-1`,
27
+ batch dims to the left.
28
+ - Add tests for new functionality. Keep one logical change per pull request.