├── .coveragerc
├── .flake8
├── .gitattributes
├── .github
├── dependabot.yml
└── workflows
│ ├── lint.yaml
│ ├── publish-to-pypi.yml
│ ├── publish-to-test-pypi.yml
│ └── tests.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── CHANGELOG.md
├── LICENSE
├── MANIFEST
├── README.md
├── docs
├── Makefile
├── make.bat
├── requirements.txt
└── source
│ ├── api-reference.rst
│ ├── conf.py
│ ├── getting-started.rst
│ ├── index.rst
│ ├── install.rst
│ └── migration.rst
├── examples
├── README.md
├── current_weather
│ ├── README.md
│ └── current_weather.py
├── postcodes_to_lat_lng
│ ├── README.md
│ └── postcodes_to_lat_lng.py
├── simple_forecast
│ ├── README.md
│ └── simple_forecast.py
├── tube_bike
│ ├── README.md
│ └── tube_bike.py
├── umbrella
│ ├── README.md
│ └── umbrella.py
└── washing
│ ├── README.md
│ └── washing.py
├── pyproject.toml
├── requirements-dev.txt
├── src
└── datapoint
│ ├── Forecast.py
│ ├── Manager.py
│ ├── __init__.py
│ ├── exceptions.py
│ └── weather_codes.py
└── tests
├── __init__.py
├── integration
└── test_manager.py
├── reference_data
├── __init__.py
├── daily_api_data.json
├── hourly_api_data.json
├── reference_data_test_forecast.py
└── three_hourly_api_data.json
└── unit
├── __init__.py
└── test_forecast.py
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | branch = True
3 | source = datapoint
4 | omit = datapoint/_version.py
5 |
6 | [report]
7 | exclude_lines =
8 | if self.debug
9 | pragma: no cover
10 | raise NotImplementedError
11 | if __name__ == .__main__.:
12 | ignore_errors = True
13 | omit =
14 | tests/*
15 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-complexity = 10
3 | max-line-length = 88
4 | extend-select = B950
5 | extend-ignore = E203,E501,E701
6 | exclude = .git,__pycache__,build,dist
7 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | datapoint/_version.py export-subst
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: pip
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 | ignore:
9 | - dependency-name: pytz
10 | versions:
11 | - "2020.5"
12 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yaml:
--------------------------------------------------------------------------------
1 | name: Lint
2 | on:
3 | pull_request:
4 | branches:
5 | - "master"
6 | jobs:
7 | black:
8 | runs-on: ubuntu-24.04
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v3
12 | - name: Set up python
13 | uses: actions/setup-python@v4
14 | with:
15 | python-version: "3.12"
16 | - name: Install and run linter
17 | run: |
18 | pip install black==22.10.0
19 | black --check --verbose --diff --color -S .
20 | isort:
21 | runs-on: ubuntu-24.04
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@v3
25 | - name: Set up python
26 | uses: actions/setup-python@v4
27 | with:
28 | python-version: "3.12"
29 | - name: Install and run linter
30 | run: |
31 | pip install isort==5.13.2
32 | isort . --check-only --diff
33 | flake8:
34 | runs-on: ubuntu-24.04
35 | steps:
36 | - name: Checkout
37 | uses: actions/checkout@v3
38 | - name: Set up python
39 | uses: actions/setup-python@v4
40 | with:
41 | python-version: "3.12"
42 | - name: Install and run linter
43 | run: |
44 | pip install flake8==7.1.0 flake8-bugbear flake8-pytest-style
45 | flake8
46 |
--------------------------------------------------------------------------------
/.github/workflows/publish-to-pypi.yml:
--------------------------------------------------------------------------------
1 | name: Publish Python 🐍 distribution 📦 to PyPI
2 | on: push
3 | jobs:
4 | build:
5 | name: Build distribution 📦
6 | runs-on: ubuntu-latest
7 | steps:
8 | - uses: actions/checkout@v4
9 | with:
10 | fetch-depth: 0
11 | fetch-tags: true
12 | - name: Set up Python
13 | uses: actions/setup-python@v5
14 | with:
15 | python-version: "3.12"
16 | - name: Install hatch
17 | uses: pypa/hatch@install
18 | - name: Build a binary wheel and a source tarball
19 | run: hatch build
20 | - name: Store the distribution packages
21 | uses: actions/upload-artifact@v4
22 | with:
23 | name: python-package-distributions
24 | path: dist/
25 | publish-to-pypi:
26 | name: Publish Python 🐍 distribution 📦 to PyPI
27 | if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
28 | needs:
29 | - build
30 | runs-on: ubuntu-latest
31 | environment:
32 | name: pypi
33 | url: https://pypi.org/p/datapoint
34 | permissions:
35 | id-token: write # IMPORTANT: mandatory for trusted publishing
36 | steps:
37 | - name: Download all the dists
38 | uses: actions/download-artifact@v4
39 | with:
40 | name: python-package-distributions
41 | path: dist/
42 | - name: Publish distribution 📦 to TestPyPI
43 | uses: pypa/gh-action-pypi-publish@release/v1
44 |
--------------------------------------------------------------------------------
/.github/workflows/publish-to-test-pypi.yml:
--------------------------------------------------------------------------------
1 | name: Publish Python 🐍 distribution 📦 to TestPyPI
2 | on: push
3 | jobs:
4 | build:
5 | name: Build distribution 📦
6 | runs-on: ubuntu-latest
7 | steps:
8 | - uses: actions/checkout@v4
9 | with:
10 | fetch-depth: 0
11 | fetch-tags: true
12 | - name: Set up Python
13 | uses: actions/setup-python@v5
14 | with:
15 | python-version: "3.12"
16 | - name: Install hatch
17 | uses: pypa/hatch@install
18 | - name: Build a binary wheel and a source tarball
19 | run: hatch build
20 | - name: Store the distribution packages
21 | uses: actions/upload-artifact@v4
22 | with:
23 | name: python-package-distributions
24 | path: dist/
25 | publish-to-testpypi:
26 | name: Publish Python 🐍 distribution 📦 to TestPyPI
27 | needs:
28 | - build
29 | runs-on: ubuntu-latest
30 | environment:
31 | name: testpypi
32 | url: https://test.pypi.org/p/datapoint
33 | permissions:
34 | id-token: write # IMPORTANT: mandatory for trusted publishing
35 | steps:
36 | - name: Download all the dists
37 | uses: actions/download-artifact@v4
38 | with:
39 | name: python-package-distributions
40 | path: dist/
41 | - name: Publish distribution 📦 to TestPyPI
42 | uses: pypa/gh-action-pypi-publish@release/v1
43 | with:
44 | repository-url: https://test.pypi.org/legacy/
45 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Run tests
2 | on: pull_request
3 | jobs:
4 | run-tests:
5 | name: Run tests on python ${{ matrix.python-version }}
6 | runs-on: ubuntu-latest
7 | strategy:
8 | matrix:
9 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
10 | steps:
11 | - uses: actions/checkout@v4
12 | - name: Set up Python
13 | uses: actions/setup-python@v5
14 | with:
15 | python-version: ${{ matrix.python-version }}
16 | - name: Install requirements
17 | run: pip install -r requirements-dev.txt
18 | - name: Run tests
19 | run: python -m pytest tests
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## OS X
2 | .DS_Store
3 | .AppleDouble
4 | .LSOverride
5 |
6 | # Icon must end with two \r
7 | Icon
8 |
9 |
10 | # Thumbnails
11 | ._*
12 |
13 | # Files that might appear on external disk
14 | .Spotlight-V100
15 | .Trashes
16 |
17 | # Directories potentially created on remote AFP share
18 | .AppleDB
19 | .AppleDesktop
20 | Network Trash Folder
21 | Temporary Items
22 |
23 | ## Python
24 | # Byte-compiled / optimized / DLL files
25 | __pycache__/
26 | *.py[cod]
27 |
28 | # C extensions
29 | *.so
30 |
31 | # Distribution / packaging
32 | .Python
33 | env/
34 | build/
35 | develop-eggs/
36 | dist/
37 | eggs/
38 | .eggs/
39 | lib/
40 | lib64/
41 | parts/
42 | sdist/
43 | var/
44 | *.egg-info/
45 | .installed.cfg
46 | *.egg
47 |
48 | # PyInstaller
49 | # Usually these files are written by a python script from a template
50 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
51 | *.manifest
52 | *.spec
53 |
54 | # Installer logs
55 | pip-log.txt
56 | pip-delete-this-directory.txt
57 |
58 | # Unit test / coverage reports
59 | htmlcov/
60 | .tox/
61 | .coverage
62 | .cache
63 | nosetests.xml
64 | coverage.xml
65 |
66 | # Translations
67 | *.mo
68 | *.pot
69 |
70 | # Django stuff:
71 | *.log
72 |
73 | # Sphinx documentation
74 | docs/_build/
75 |
76 | # PyBuilder
77 | target/
78 |
79 | # vi/vim
80 | *.swp
81 |
82 | # mkdocs
83 | site/
84 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | # See https://pre-commit.com for more information
2 | # See https://pre-commit.com/hooks.html for more hooks
3 | repos:
4 | - repo: https://github.com/pre-commit/pre-commit-hooks
5 | rev: v5.0.0
6 | hooks:
7 | - id: trailing-whitespace
8 | - id: end-of-file-fixer
9 | - id: check-yaml
10 | - id: check-added-large-files
11 | - repo: https://github.com/pycqa/isort
12 | rev: 5.13.2
13 | hooks:
14 | - id: isort
15 | - repo: https://github.com/pycqa/flake8
16 | rev: 7.1.0
17 | hooks:
18 | - id: flake8
19 | additional_dependencies:
20 | - flake8-bugbear==24.2.6
21 | - flake8-pytest-style==2.0.0
22 | - repo: https://github.com/psf/black
23 | rev: 22.10.0
24 | hooks:
25 | - id: black
26 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # Read the Docs configuration file for Sphinx projects
2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3 |
4 | # Required
5 | version: 2
6 |
7 | # Set the OS, Python version and other tools you might need
8 | build:
9 | os: ubuntu-22.04
10 | tools:
11 | python: "3.12"
12 |
13 | # Build documentation in the "docs/" directory with Sphinx
14 | sphinx:
15 | configuration: "docs/source/conf.py"
16 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
17 | # builder: "dirhtml"
18 | # Fail on all warnings to avoid broken references
19 | # fail_on_warning: true
20 |
21 |
22 | # Optional but recommended, declare the Python requirements required
23 | # to build your documentation
24 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
25 | python:
26 | install:
27 | - requirements: "docs/requirements.txt"
28 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
4 |
5 | ## [Unreleased]
6 |
7 | ## [0.13.0] - 2025-01-15
8 |
9 | + Remove python 3.8 support
10 | + Update GitHub location in docs
11 |
12 | ## [0.12.1] - 2024-11-28
13 |
14 | + Fix bug in API request when twice-daily forecast was used.
15 |
16 | ## [0.12.0] - 2024-11-27
17 |
18 | + Use one timestep per day for daily forecasts.
19 | + Add twice-daily forecast option to split daily forecasts into day and night.
20 | + No longer strip 'day', 'night', 'midday', 'midnight' from element names in daily and twice-daily forecasts.
21 |
22 | ## [0.11.0] - 2024-11-26
23 |
24 | + Correct elements to camelCase for daily forecasts.
25 | + Add option to convert numeric significant weather code to string description
26 |
27 | ## [0.10.0] - 2024-11-17
28 |
29 | + Modernise packaging and build tooling and infrastructure.
30 | + Migrate to use new MetOffice DataHub. This required many changes, for more
31 | details see the 'migration' page of the documentation.
32 |
33 | ## [0.9.9] - 2024-02-09
34 |
35 | + Update versioneer
36 | + Add pythons 3.9, 3.10, 3.11, 3.12 to tests and setup.py.
37 | + Remove support for python < 3.8
38 | + Remove deprecated `new_old` and `future_old` functions.
39 | + Add `__str__` functions to `Timestep`, `Element`, `Day`, `Site`
40 | + Add element to `Forecast` to track if forecast is daily or 3 hourly
41 | + Change `id` variable in `Forecast`, `Observation`, `Site` to `location_id`.
42 | + Change `id` variable in `Element` to `field_code`.
43 |
44 | ## [0.9.8] - 2020-07-03
45 |
46 | + Remove f-string in test
47 |
48 | ## [0.9.7] - 2020-07-03
49 |
50 | + Bugfix for `get_observation_sites`
51 |
52 | ## [0.9.6] - 2020-05-05
53 |
54 | + Require arguments to `get_nearest_forecast_site` and `get_nearest_observation_site`.
55 | + Add python 3.8 to tests and setup.py
56 |
57 | ## [0.9.5] - 2019-10-01
58 |
59 | + Remove support for Python 3.4.
60 |
61 | ## [0.9.4] - 2019-09-10
62 |
63 | + Fix to url case in `travis.yml` to enable releases.
64 |
65 | ## [0.9.3] - 2019-09-10
66 |
67 | + Update README.md and travis.yml due to change in ownership.
68 |
69 | ## [0.9.2] - 2019-07-26
70 |
71 | + Raise an error if data for the requested location is not provided from the datapoint API.
72 |
73 | ## [0.9.1] - 2019-05-21
74 |
75 | + Remove stray print statement
76 |
77 | ## [0.9.0] - 2019-05-18
78 |
79 | + Explicitly state the use of semantic versioning in `README.md`.
80 | + Add `elements()` function to `Timestep`.
81 | + Remove night/day indication from weather codes which have them.
82 | + Change the logic used to calculate the closest timestep to a datetime. The closest timestep to the datetime is now used. Add a new function, `Forecast.at_datetime(target)` to do this. `Forecast.now()` has been changed to use this new function. The old behaviour is deprecated and available using `Forecast.now_old()`. `Forecast.future()` has been changed to use this new function. The old behaviour is deprecated and available using `Forecast.future_old()`.
83 | + Check if keys are returned from datapoint api in `Manager.py`. Do not attempt to read the values from the dict if they are not there.
84 |
85 | ## [0.8.0] - 2019-04-05
86 |
87 | + Retry the connection to datapoint if it fails (up to 10 times).
88 | + Use versioneer to set version number from git tag.
89 | + Fix failure to return forecast at midnight.
90 | + Add changelog.
91 |
92 | ## [0.7.0] - 2019-02-19
93 |
94 | + Check that data is provided in tests.
95 | + Set weather element of `timestep` to 'not reported' if data is not provided.
96 | + Update examples to use `get_nearest_forecast_site` function.
97 | + Rename `get_all_sites()` to `get_forecast_sites()` and `get_nearest_site()` to `get_nearest_forecast_site()`.
98 | + Limit observations to sites within 20 km of the nearest observation site.
99 | + Require that the nearest location is within 30 km of the requested location.
100 | + Show the available sites on maps in the documentation.
101 | + Use a haversine function to calculate the distance between coordinates.
102 | + Use setuptools in `setup.py`.
103 | + Fix bug where site attributes were assigned incorrectly.
104 | + Use sphinx to generate documentation.
105 | + Fix bug where longitude or latitude values of 0 returned false in `get_nearest_site()`.
106 |
107 | ## [0.6.1] - 2019-01-26
108 |
109 | + Remove stray print statements.
110 |
111 | ## [0.6.0] - 2019-01-26
112 |
113 | + Remove support for python 2 and python 3.3.
114 |
115 | ## [0.5.1] - 2019-01-26
116 |
117 | + Correct wrong version number.
118 |
119 | ## [0.5.0] - 2019-01-26
120 |
121 | + Fix latitude and longitude in `manager_test.py`.
122 | + Add support for observations.
123 | + Swap the order of latitude and longitude in function calls.
124 | + Add a timeout of 1 second to the API call.
125 | + Fix error which set sites to `None`.
126 | + Fix documentation build.
127 | + Use python 3 syntax in examples.
128 | + Fix bug where `forecast.now()` always returned `None`.
129 | + Change print statements in `Manager.py` and `Forecast.py` to python 3 style.
130 | + Fix bug where no data was returned for about an hour after midnight.
131 | + Add `forecast.future()` function.
132 | + Add support for python 3.6.
133 |
134 | ## [0.4.3] - 2017-01-19
135 |
136 | + Use a custom error when datapoint call fails.
137 |
138 | ## [0.4.2] - 2017-01-18
139 |
140 | + Only send python 3.5 to Travis.
141 |
142 | ## [0.4.1] - 2017-01-04
143 |
144 | + Update tests.
145 | + Fix bug with `forecast.now()`.
146 | + Implement text forecast.
147 |
148 | ## [0.4.0] - 2016-06-06
149 |
150 | + Add python 3 support.
151 |
152 | ## [0.3.0] - 2016-01-06
153 |
154 | + Use python datetime for dates and times.
155 | + Add instructions for installing from master using pip.
156 |
157 | ## [0.2.2] - 2014-10-24
158 |
159 | + Add examples
160 | + Use readthedocs.
161 | + Add error when no data is returned.
162 | + Cache site requests for an hour.
163 |
164 | ## [0.2.1] - 2014-10-17
165 |
166 | + Test string to int conversion.
167 |
168 | ## [0.2] - 2014-10-10
169 |
170 | + Use travis
171 | + Add concept of API key profiles.
172 | + Fix type casting.
173 | + Add `forecast.now()` function.
174 |
175 | ## [0.1] - 2014-07-16
176 |
177 | + Initial commit and license.
178 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 | Datapoint Python, a Python library for the Met Office Datapoint API.
635 | Copyright (C) 2016 Jacob Tomlinson
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Datapoint Python Copyright (C) 2016 Jacob Tomlinson
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/MANIFEST:
--------------------------------------------------------------------------------
1 | # file GENERATED by distutils, do NOT edit
2 | setup.py
3 | datapoint/Day.py
4 | datapoint/Element.py
5 | datapoint/Forecast.py
6 | datapoint/Manager.py
7 | datapoint/Observation.py
8 | datapoint/Site.py
9 | datapoint/Timestep.py
10 | datapoint/__init__.py
11 | datapoint/exceptions.py
12 | datapoint/profile.py
13 | datapoint/regions/RegionManager.py
14 | datapoint/regions/__init__.py
15 | datapoint/regions/region_names.py
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # _DataPoint for Python_
2 | [](https://pypi.python.org/pypi/datapoint/)
3 | [](https://pypi.python.org/pypi/datapoint/)
4 | [](https://readthedocs.org/projects/datapoint-python/)
5 |
6 |
7 | _A Python module for accessing weather data via the [Met Office](http://www.metoffice.gov.uk/)'s open data API
8 | known as [DataPoint](http://www.metoffice.gov.uk/datapoint)._
9 |
10 | __For personal reasons I have changed my GitHub username. The repository location has changed. The homepage link on PyPi is up-to-date__
11 |
12 | __Disclaimer: This module is in no way part of the DataPoint project/service.
13 | This module is intended to simplify the use of DataPoint for small Python projects (e.g school projects).
14 | No support for this module is provided by the Met Office and may break as the DataPoint service grows/evolves.
15 | The author will make reasonable efforts to keep it up to date and fully featured.__
16 |
17 | ## Features
18 | * List forecast sites
19 | * Get nearest forecast site from latitiude and longitude
20 | * Get the following 5 day forecast types for any site
21 | * Daily (Two timesteps, midday and midnight UTC)
22 | * 3 hourly (Eight timesteps, every 3 hours starting at midnight UTC)
23 |
24 | ## Installation
25 |
26 | ```Bash
27 | $ pip install DataPoint
28 | ```
29 |
30 | You will also require a [DataPoint API key](http://www.metoffice.gov.uk/datapoint/API).
31 |
32 | For more installation methods see the [installation guide](http://datapoint-python.readthedocs.org/en/latest/install/).
33 |
34 | ## Documentation
35 |
36 | Detailed documentation for this project is available on [Read the Docs](http://datapoint-python.readthedocs.org/en/latest). This project uses semantic versioning as defined at [semver.org](https://semver.org/).
37 |
38 | ## Example Usage
39 |
40 | ```Python
41 | import datapoint
42 |
43 | # Create connection to DataPoint with your API key
44 | conn = datapoint.connection(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
45 |
46 | # Get the nearest site for my latitude and longitude
47 | site = conn.get_nearest_forecast_site(51.500728, -0.124626)
48 |
49 | # Get a forecast for my nearest site with 3 hourly timesteps
50 | forecast = conn.get_forecast_for_site(site.location_id, "3hourly")
51 |
52 | # Get the current timestep from the forecast
53 | current_timestep = forecast.now()
54 |
55 | # Print out the site and current weather
56 | print(site.name + "-" + current_timestep.weather.text)
57 |
58 | ```
59 |
60 | Example output
61 | ```
62 | London - Heavy rain
63 | ```
64 |
65 | See [examples directory](https://github.com/Perseudonymous/datapoint-python/tree/master/examples) for more in depth examples.
66 |
67 | ## Contributing changes
68 |
69 | Please feel free to submit issues and pull requests.
70 |
71 | ## License
72 |
73 | GPLv3
74 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = source
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 |
13 | %SPHINXBUILD% >NUL 2>NUL
14 | if errorlevel 9009 (
15 | echo.
16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17 | echo.installed, then set the SPHINXBUILD environment variable to point
18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
19 | echo.may add the Sphinx directory to PATH.
20 | echo.
21 | echo.If you don't have Sphinx installed, grab it from
22 | echo.https://www.sphinx-doc.org/
23 | exit /b 1
24 | )
25 |
26 | if "%1" == "" goto help
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | sphinx
2 | -e .
3 |
--------------------------------------------------------------------------------
/docs/source/api-reference.rst:
--------------------------------------------------------------------------------
1 | API reference
2 | =============
3 |
4 | .. automodule:: datapoint.Manager
5 | :members:
6 |
7 | .. automodule:: datapoint.Forecast
8 | :members:
9 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # For the full list of built-in configuration values, see the documentation:
4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
5 |
6 | # -- Project information -----------------------------------------------------
7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8 | import importlib
9 |
10 | project = "datapoint-python"
11 | copyright = "2024, Emily Price, Jacob Tomlinson"
12 | author = "Emily Price, Jacob Tomlinson"
13 |
14 | release = importlib.metadata.version("datapoint")
15 | version = importlib.metadata.version("datapoint")
16 |
17 | # -- General configuration ---------------------------------------------------
18 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
19 |
20 | extensions = [
21 | "sphinx.ext.autodoc",
22 | ]
23 |
24 | templates_path = ["_templates"]
25 | exclude_patterns = []
26 |
27 |
28 | # -- Options for HTML output -------------------------------------------------
29 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
30 |
31 | html_theme = "alabaster"
32 | html_static_path = ["_static"]
33 |
--------------------------------------------------------------------------------
/docs/source/getting-started.rst:
--------------------------------------------------------------------------------
1 | Getting started
2 | ===============
3 |
4 | Getting started with DataHub for Python is simple and you can write a
5 | simple script which prints out data in just 6 lines of Python.
6 |
7 | API Key
8 | -------
9 |
10 | To access DataPoint you need to `register `__
11 | with the Met Office and get yourself an API key. The process is simple and just
12 | ensures that you don’t abuse the service. You will need access to the
13 | Site-Specific forecast API.
14 |
15 | Connecting to DataHub
16 | -----------------------
17 |
18 | Now that you have an API key you can import the module:
19 |
20 | ::
21 |
22 | import datapoint
23 |
24 | And create a connection to DataHub:
25 |
26 | ::
27 |
28 | manager = datapoint.Manager(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
29 |
30 | This creates a `manager` object which manages the connection and interacts
31 | with DataHub.
32 |
33 | Getting data from DataHub
34 | ---------------------------
35 |
36 | So now that you have a Manager object with a connection to DataHub you can
37 | request some data. To do this, use the `manager` object:
38 |
39 | ::
40 |
41 | forecast = manager.get_forecast(51, 0, "hourly", convert_weather_code=True)
42 |
43 | This takes four parameters: the latitude and longitude of the location you want
44 | a forecast for, a forecast type of “hourly” and an instruction to convert the
45 | numeric weather code to a string description. We’ll discuss the forecast types
46 | later on.
47 |
48 | This Forecast Object which has been returned to us contains lots of information
49 | which we will cover in a later section, right now we’re just going to get the
50 | data for the current time:
51 |
52 | ::
53 |
54 | current_weather = forecast.now()
55 |
56 | This is a dict which contains many different details about the weather
57 | but for now we’ll just print out one field.
58 |
59 | ::
60 |
61 | print(current_weather["feelsLikeTemperature"])
62 |
63 | And there you have it. If you followed all the steps you should have
64 | printed out the current weather for your chosen location.
65 |
66 | Further Examples
67 | ----------------
68 |
69 | For more code examples please have a look in the `examples
70 | folder `__
71 | in the GitHub project.
72 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. datapoint-python documentation master file, created by
2 | sphinx-quickstart on Tue Nov 12 17:56:23 2024.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | datapoint-python documentation
7 | ==============================
8 |
9 | .. toctree::
10 | :maxdepth: 2
11 | :caption: Contents:
12 |
13 | install
14 | getting-started
15 | migration
16 | api-reference
17 |
--------------------------------------------------------------------------------
/docs/source/install.rst:
--------------------------------------------------------------------------------
1 | Installation
2 | ============
3 |
4 | DataPoint for Python can be installed like any other Python module.
5 |
6 | It is available on `PyPI `__
7 | and the source is available on
8 | `GitHub `__.
9 |
10 | Pip
11 | ---
12 |
13 | `Pip `__ makes Python package installation simple.
14 | For the latest stable version just fire up your terminal and run:
15 |
16 | ::
17 |
18 | pip install datapoint
19 |
20 | or for the very latest code from the repository’s master branch run:
21 |
22 | ::
23 |
24 | pip install git+git://github.com/perseudonymous/datapoint-python.git@master
25 |
26 | and to upgrade it in the future:
27 |
28 | ::
29 |
30 | pip install git+git://github.com/perseudonymous/datapoint-python.git@master --upgrade
31 |
32 | Source
33 | ------
34 |
35 | You can also install from the source in GitHub.
36 |
37 | First checkout the GitHub repository (or you can `download the
38 | zip `__
39 | and extract it).
40 |
41 | ::
42 |
43 | git clone https://github.com/perseudonymous/datapoint-python.git datapoint-python
44 |
45 | Navigate to that directory
46 |
47 | ::
48 |
49 | cd datapoint-python
50 |
51 | Then run the setup
52 |
53 | ::
54 |
55 | python setup.py install
56 |
--------------------------------------------------------------------------------
/docs/source/migration.rst:
--------------------------------------------------------------------------------
1 | Migration from DataPoint
2 | ========================
3 |
4 | The new APIs the Met Office provide via DataHub are very different in behaviour
5 | to the old APIs which were provided via DataPoint. As such this library has
6 | changed greatly.
7 |
8 | The main changes are below.
9 |
10 | No concept of 'sites'
11 | ---------------------
12 |
13 | There is no concept of retrieving a site id for a location before requesting a
14 | forecast. Now a latitude and longitude are provided to the library directly.
15 |
16 | No observations
17 | ---------------
18 |
19 | The new API does not provide 'observations' like DataPoint. However, the current
20 | state of the weather is returned as part of the forecast responses. As such,
21 | this library no longer provides separate 'observations'.
22 |
23 | Simplified object hierarchy
24 | ---------------------------
25 |
26 | Python dicts are used instead of classes to allow more flexibility with handling
27 | data returned from the MetOffice API, and because new MetOffice API provides
28 | data with a more convenient structure. The concept of 'Days' has also been
29 | removed from the library and instead all time steps are provided in one list.
30 | The data structure for a single time step is::
31 |
32 | {
33 | 'time': datetime.datetime(2024, 2, 19, 13, 0, tzinfo=datetime.timezone.utc),
34 | 'screenTemperature': {
35 | 'value': 10.09,
36 | 'description': 'Screen Air Temperature',
37 | 'unit_name': 'degrees Celsius',
38 | 'unit_symbol': 'Cel'
39 | },
40 | 'screenDewPointTemperature': {
41 | 'value': 8.08,
42 | 'description': 'Screen Dew Point Temperature',
43 | 'unit_name': 'degrees Celsius',
44 | 'unit_symbol': 'Cel'
45 | },
46 | 'feelsLikeTemperature': {
47 | 'value': 6.85,
48 | 'description': 'Feels Like Temperature',
49 | 'unit_name': 'degrees Celsius',
50 | 'unit_symbol': 'Cel'
51 | },
52 | 'windSpeed10m': {
53 | 'value': 7.57,
54 | 'description': '10m Wind Speed',
55 | 'unit_name': 'metres per second',
56 | 'unit_symbol': 'm/s'
57 | },
58 | 'windDirectionFrom10m': {
59 | 'value': 263,
60 | 'description': '10m Wind From Direction',
61 | 'unit_name': 'degrees',
62 | 'unit_symbol': 'deg'
63 | },
64 | 'windGustSpeed10m': {
65 | 'value': 12.31,
66 | 'description': '10m Wind Gust Speed',
67 | 'unit_name': 'metres per second',
68 | 'unit_symbol': 'm/s'
69 | },
70 | 'visibility': {
71 | 'value': 21201,
72 | 'description': 'Visibility',
73 | 'unit_name': 'metres',
74 | 'unit_symbol': 'm'
75 | },
76 | 'screenRelativeHumidity': {
77 | 'value': 87.81,
78 | 'description': 'Screen Relative Humidity',
79 | 'unit_name': 'percentage',
80 | 'unit_symbol': '%'
81 | },
82 | 'mslp': {
83 | 'value': 103080,
84 | 'description': 'Mean Sea Level Pressure',
85 | 'unit_name': 'pascals',
86 | 'unit_symbol': 'Pa'
87 | },
88 | 'uvIndex': {
89 | 'value': 1,
90 | 'description': 'UV Index',
91 | 'unit_name': 'dimensionless',
92 | 'unit_symbol': '1'
93 | },
94 | 'significantWeatherCode': {
95 | 'value': 'Cloudy',
96 | 'description': 'Significant Weather Code',
97 | 'unit_name': 'dimensionless',
98 | 'unit_symbol': '1'
99 | },
100 | 'precipitationRate': {
101 | 'value': 0.0,
102 | 'description': 'Precipitation Rate',
103 | 'unit_name': 'millimetres per hour',
104 | 'unit_symbol': 'mm/h'
105 | },
106 | 'probOfPrecipitation': {
107 | 'value': 21,
108 | 'description': 'Probability of Precipitation',
109 | 'unit_name': 'percentage',
110 | 'unit_symbol': '%'
111 | }
112 | }
113 |
114 | Different data provided
115 | -----------------------
116 |
117 | There are some differences in what data are provided in each weather forecast
118 | compared to the old DataPoint API, and in the names of the features.
119 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 |
4 | ## Getting started
5 |
6 | _Examples of getting data from DataPoint._
7 |
8 | * [Current Weather](current_weather/) - Get the current weather for a specified
9 | latitude and longitude.
10 |
11 | * [Simple Forecast](simple_forecast/) - Get a full 5 day forecast for a specified
12 | latitude and longitude.
13 |
14 | ## Making Decisions
15 |
16 | _Examples which make decisions based on weather data._
17 |
18 | * [Umbrella](umbrella/) - Inform the user whether they need an umbrella today
19 | for a specified latitude and longitude.
20 |
21 | * [Washing](washing/) - Inform the user which day in the next 5 days would be
22 | the best for hanging out their washing for a specified latitude and longitude.
23 |
24 | ## Mixing Data
25 |
26 | _Examples which makes use of other API's in conjunction with DataPoint._
27 |
28 | * [Tube or Bike](tube_bike/) - Inform the user whether they should cycle or
29 | take the tube across London, based on weather and tube service for two sets of
30 | longitude and latitude along with a tube line name.
31 |
32 | * [Postcodes](postcodes_to_lat_lng) - A variation on the [Current Weather](current_weather/)
33 | example but uses UK postcodes instead of latitude and longitude.
34 |
--------------------------------------------------------------------------------
/examples/current_weather/README.md:
--------------------------------------------------------------------------------
1 | # Current Weather
2 |
3 | _This example displays the current weather and temperature for your location._
4 |
5 | ### Required Modules
6 | * [datapoint](https://github.com/perseudonymous/datapoint-python)
7 |
8 | ## Learning Objective
9 |
10 | Learn how to simply connect to DataPoint and print out some data.
11 |
12 | ## Example Usage
13 |
14 | ```Shell
15 | python current_weather.py
16 | ```
17 |
18 | ## Example Output
19 |
20 | ```
21 | London
22 | Cloudy
23 | 18°C
24 | ```
25 |
--------------------------------------------------------------------------------
/examples/current_weather/current_weather.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | This is a simple example which will print out the current weather and
4 | temperature for our location.
5 | """
6 |
7 | import datapoint
8 |
9 | # Create datapoint connection
10 | manager = datapoint.Manager(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
11 |
12 | # Get a forecast for the nearest location
13 | forecast = manager.get_forecast(51.500728, -0.124626, "hourly")
14 |
15 | # Get the current timestep using now() and print out some info
16 | now = forecast.now()
17 | print(now["significantWeatherCode"])
18 | print(f"{now['screenTemperature']['value']} {now['screenTemperature']['unit_symbol']}")
19 |
--------------------------------------------------------------------------------
/examples/postcodes_to_lat_lng/README.md:
--------------------------------------------------------------------------------
1 | # Current Weather
2 |
3 | _A variation on current_weather.py which uses postcodes rather than lon lat._
4 |
5 | ### Required Modules
6 | * [datapoint](https://github.com/perseudonymous/datapoint-python)
7 | * [python-postcodes-io](https://github.com/raigad/python-postcodes-io)
8 |
9 | ## Learning Objective
10 |
11 | Learn how to make use of the postcodes module to make it even simpler to access
12 | Met Office data.
13 |
14 | ## Example Usage
15 |
16 | ```Shell
17 | python postcodes_to_lat_lng.py
18 | ```
19 |
20 | ## Example Output
21 |
22 | ```
23 | Horseguards Parade
24 | Overcast
25 | 15°C
26 | ```
27 |
--------------------------------------------------------------------------------
/examples/postcodes_to_lat_lng/postcodes_to_lat_lng.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | A variation on current_weather.py which uses postcodes rather than lon lat.
4 | """
5 |
6 | import postcodes_io_api
7 |
8 | import datapoint
9 |
10 | # Create datapoint connection
11 | manager = datapoint.Manager(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
12 |
13 |
14 | # Get longitude and latitude from postcode
15 | postcodes_conn = postcodes_io_api.Api()
16 | postcode = postcodes_conn.get_postcode("SW1A 2AA")
17 | latitude = postcode["result"]["latitude"]
18 | longitude = postcode["result"]["longitude"]
19 |
20 | # Get a forecast for the nearest site
21 | forecast = manager.get_forecast(longitude, latitude, "hourly")
22 |
23 | # Get the current timestep using now() and print out some info
24 | now = forecast.now()
25 | print(now["significantWeatherCode"])
26 | print(f"{now['screenTemperature']['value']} {now['screenTemperature']['unit_symbol']}")
27 |
--------------------------------------------------------------------------------
/examples/simple_forecast/README.md:
--------------------------------------------------------------------------------
1 | # Simple Forecast
2 |
3 | _This example gets a 5 day forecast for your location and prints out
4 | some values from each timestep for each day._
5 |
6 | ### Required Modules
7 | * [datapoint](https://github.com/perseudonymous/datapoint-python)
8 |
9 | ## Learning Objective
10 |
11 | Explore the day and timestep objects and loop through them.
12 |
13 | ## Example Usage
14 |
15 | ```Shell
16 | python simple_forecast.py
17 | ```
18 |
19 | ## Example Output
20 |
21 | ```
22 | London
23 |
24 | 2014-10-18Z
25 | 180
26 | Partly cloudy (night)
27 | 17°C
28 | 360
29 | Cloudy
30 | 18°C
31 | 540
32 | Light rain shower (day)
33 | 18°C
34 | 720
35 | Light rain
36 | 18°C
37 | 900
38 | Cloudy
39 | 19°C
40 | 1080
41 | Cloudy
42 | 18°C
43 | 1260
44 | Cloudy
45 | 18°C
46 |
47 | 2014-10-19Z
48 | 0
49 | Cloudy
50 | 18°C
51 | 180
52 | Light rain
53 | 17°C
54 | 360
55 | Heavy rain
56 | 17°C
57 | 540
58 | Cloudy
59 | 17°C
60 | 720
61 | Cloudy
62 | 18°C
63 | 900
64 | Partly cloudy (day)
65 | 18°C
66 | 1080
67 | Partly cloudy (night)
68 | 17°C
69 | 1260
70 | Overcast
71 | 15°C
72 |
73 | 2014-10-20Z
74 | 0
75 | Overcast
76 | 14°C
77 | 180
78 | Light rain shower (night)
79 | 14°C
80 | 360
81 | Partly cloudy (night)
82 | 13°C
83 | 540
84 | Partly cloudy (day)
85 | 13°C
86 | 720
87 | Cloudy
88 | 15°C
89 | 900
90 | Cloudy
91 | 15°C
92 | 1080
93 | Cloudy
94 | 14°C
95 | 1260
96 | Cloudy
97 | 13°C
98 |
99 | 2014-10-21Z
100 | 0
101 | Cloudy
102 | 13°C
103 | 180
104 | Light rain shower (night)
105 | 13°C
106 | 360
107 | Light rain shower (night)
108 | 14°C
109 | 540
110 | Light rain shower (day)
111 | 13°C
112 | 720
113 | Light rain shower (day)
114 | 13°C
115 | 900
116 | Sunny day
117 | 13°C
118 | 1080
119 | Clear night
120 | 12°C
121 | 1260
122 | Partly cloudy (night)
123 | 10°C
124 |
125 | 2014-10-22Z
126 | 0
127 | Clear night
128 | 10°C
129 | 180
130 | Clear night
131 | 9°C
132 | 360
133 | Partly cloudy (night)
134 | 9°C
135 | 540
136 | Cloudy
137 | 10°C
138 | 720
139 | Cloudy
140 | 13°C
141 | 900
142 | Cloudy
143 | 15°C
144 | 1080
145 | Overcast
146 | 14°C
147 | 1260
148 | Overcast
149 | 13°C
150 | ```
151 |
--------------------------------------------------------------------------------
/examples/simple_forecast/simple_forecast.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | This example will print out a simple forecast for the next 5 days.
4 | It will allow us to explore the day, timestep and element objects.
5 | """
6 |
7 | import datetime
8 |
9 | import datapoint
10 |
11 | # Create datapoint connection
12 | manager = datapoint.Manager(api_key="api key goes here")
13 |
14 |
15 | forecast = manager.get_forecast(51.500728, -0.124626, frequency="hourly")
16 |
17 | # Loop through timesteps and print information
18 | for timestep in forecast.timesteps:
19 | print(timestep["time"])
20 | print(timestep["significantWeatherCode"]["value"])
21 | print(
22 | "{temp} {temp_units}".format(
23 | temp=timestep["screenTemperature"]["value"],
24 | temp_units=timestep["screenTemperature"]["unit_symbol"],
25 | )
26 | )
27 |
28 | print(forecast.now())
29 |
30 | print(forecast.at_datetime(datetime.datetime(2024, 2, 11, 14, 0)))
31 |
--------------------------------------------------------------------------------
/examples/tube_bike/README.md:
--------------------------------------------------------------------------------
1 | # Bike or Tube
2 |
3 | _This example uses open weather data in conjunction with open transport data
4 | for London to advise you on whether you should cycle or catch the tube around
5 | London._
6 |
7 | ### Required Modules
8 | * [datapoint](https://github.com/perseudonymous/datapoint-python)
9 | * [tubestatus](https://github.com/jacobtomlinson/tube-status)
10 |
11 | ## Learning Objective
12 |
13 | Discover how to mix multiple data sources together.
14 |
15 | ## Example Usage
16 |
17 | ```Shell
18 | python tube_bike.py
19 | ```
20 |
21 | ## Example Output
22 |
23 | ```
24 | Bad service on the tube, cycling it is!
25 | ```
26 |
--------------------------------------------------------------------------------
/examples/tube_bike/tube_bike.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | This one's for Londoners. Get the weather for home and work and get the tube status
4 | for your usual line. Then use that information to decide whether you're better off
5 | cycling or catching the tube.
6 | """
7 |
8 | import tubestatus
9 |
10 | import datapoint
11 |
12 | # Create datapoint connection
13 | manager = datapoint.Manager(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
14 |
15 | # Get a forecast for my house and work
16 | my_house_forecast = manager.get_forecast(51.5016730, 0.0057500, "hourly")
17 | work_forecast = manager.get_forecast(51.5031650, -0.1123050, "hourly")
18 |
19 | # Get the current timestep for both locations
20 | my_house_now = my_house_forecast.now()
21 | work_now = work_forecast.now()
22 |
23 | # Create a tube status connection
24 | current_status = tubestatus.Status()
25 |
26 | # Get the status of the Waterloo and City line
27 | waterloo_status = current_status.get_status("Waterloo and City")
28 |
29 | # Check whether there are any problems with rain or the tube
30 | if (
31 | my_house_now["probOfPrecipitation"]["value"] < 40
32 | and work_now["probOfPrecipitation"]["value"] < 40
33 | and waterloo_status.description == "Good Service"
34 | ):
35 | print("Rain is unlikely and tube service is good, the decision is yours.")
36 |
37 | # If it is going to rain then suggest the tube
38 | elif (
39 | my_house_now["probOfPrecipitation"]["value"] >= 40
40 | or work_now["probOfPrecipitation"]["value"] >= 40
41 | ) and waterloo_status.description == "Good Service":
42 | print("Looks like rain, better get the tube")
43 |
44 | # If the tube isn't running then suggest cycling
45 | elif (
46 | my_house_now["probOfPrecipitation"]["value"] < 40
47 | and work_now["probOfPrecipitation"]["value"] < 40
48 | and waterloo_status.description != "Good Service"
49 | ):
50 | print("Bad service on the tube, cycling it is!")
51 |
52 | # Else if both are bad then suggest cycling in the rain
53 | else:
54 | print(
55 | "The tube has poor service so you'll have to cycle,"
56 | " but it's raining so take your waterproofs."
57 | )
58 |
--------------------------------------------------------------------------------
/examples/umbrella/README.md:
--------------------------------------------------------------------------------
1 | # Do I need an umbrella
2 |
3 | _This example checks the forecast for your current location to see if it is going
4 | to rain at any stage today. If so it will suggest that you take an umbrella._
5 |
6 | ### Required Modules
7 | * [datapoint](https://github.com/perseudonymous/datapoint-python)
8 |
9 | ## Learning Objective
10 |
11 | Make a simple decision based on the data provided by DataPoint.
12 |
13 | ## Example Usage
14 |
15 | ```Shell
16 | python umbrella.py
17 | ```
18 |
19 | ## Example Output
20 |
21 | ```
22 | London
23 | Looks like rain! Better take an umbrella.
24 | ```
25 |
--------------------------------------------------------------------------------
/examples/umbrella/umbrella.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | This example checks whether it is due to rain at any point
4 | today and then decides if we need to take an umbrella.
5 | """
6 |
7 | import datetime
8 |
9 | import datapoint
10 |
11 | # Create umbrella variable to use later
12 | umbrella = False
13 |
14 | # Create datapoint connection
15 | manager = datapoint.Manager(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
16 |
17 | # Get a forecast for the nearest site
18 | forecast = manager.get_forecast(51.500728, -0.124626, "hourly")
19 |
20 | # Loop through all the timesteps in day 0 (today)
21 | for timestep in forecast.timesteps:
22 | # Check to see if the chance of rain is more than 20% at any point
23 | if (
24 | timestep["probOfPrecipitation"]["value"] > 20
25 | and timestep["time"].date == datetime.date.now()
26 | ):
27 | umbrella = True
28 |
29 | # Print out the results
30 | if umbrella is True:
31 | print("Looks like rain! Better take an umbrella.")
32 | else:
33 | print("Don't worry you don't need an umbrella today.")
34 |
--------------------------------------------------------------------------------
/examples/washing/README.md:
--------------------------------------------------------------------------------
1 | # When should I do my washing?
2 |
3 | _This example looks at the forecast for the next 5 days and suggests which day
4 | (if any) would be best for hanging your washing out to dry._
5 |
6 | ### Required Modules
7 | * [datapoint](https://github.com/perseudonymous/datapoint-python)
8 |
9 | ## Learning Objective
10 |
11 | Make a slightly more complex decision by comparing values provided by DataPoint.
12 |
13 | ## Example Usage
14 |
15 | ```Shell
16 | python washing.py
17 | ```
18 |
19 | ## Example Output
20 |
21 | ```
22 | London
23 | Monday is the best day with a score of 26
24 | ```
25 |
--------------------------------------------------------------------------------
/examples/washing/washing.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | This example will tell us which day would be best to hang out
4 | our washing to dry.
5 |
6 | We will loop over the next 5 days and decide whether it is
7 | ok to hang out the washing. Then for the good days we will rank
8 | them and print out the best.
9 | """
10 |
11 | import datapoint
12 |
13 | # Set thresholds
14 | MAX_WIND = 31 # in mph. We don't want the washing to blow away
15 | MAX_PRECIPITATION = 20 # Max chance of rain we will accept
16 |
17 | # Variables for later
18 | best_time = None
19 | best_score = 0 # For simplicity the score will be temperature + wind speed
20 |
21 | # Create datapoint connection
22 | manager = datapoint.Manager(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
23 |
24 | # Get a forecast for the nearest site
25 | forecast = manager.get_forecast(51.500728, -0.124626, "daily")
26 |
27 | # Loop through days
28 | for day in forecast.days:
29 | # Get the 'Day' timestep
30 | if day.timesteps[0].name == "Day":
31 | timestep = day.timesteps[0]
32 |
33 | # If precipitation, wind speed and gust are less than their threshold
34 | if (
35 | timestep.precipitation.value < MAX_PRECIPITATION
36 | and timestep.wind_speed.value < MAX_WIND
37 | and timestep.wind_gust.value < MAX_WIND
38 | ):
39 | # Calculate the score for this timestep
40 | timestep_score = timestep.wind_speed.value + timestep.temperature.value
41 |
42 | # If this timestep scores better than the current best replace it
43 | if timestep_score > best_score:
44 | best_score = timestep_score
45 | best_day = day.date
46 |
47 | for timestep in forecast.timesteps:
48 | # If precipitation, wind speed and gust are less than their threshold
49 | if (
50 | timestep.precipitation.value < MAX_PRECIPITATION
51 | and timestep.wind_speed.value < MAX_WIND
52 | and timestep.wind_gust.value < MAX_WIND
53 | ):
54 | # Calculate the score for this timestep
55 | timestep_score = (
56 | timestep["windSpeed10m"]["value"] + timestep["screenTemperature"]["value"]
57 | )
58 |
59 | # If this timestep scores better than the current best replace it
60 | if timestep_score > best_score:
61 | best_score = timestep_score
62 | best_time = timestep["time"]
63 |
64 |
65 | # If best_day is still None then there are no good days
66 | if best_time is None:
67 | print("Better use the tumble dryer")
68 |
69 | # Otherwise print out the day
70 | else:
71 | print(f"{best_time} is the best day with a score of {best_score}")
72 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling", "versioningit"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "datapoint"
7 | dynamic = ["version"]
8 | authors = [
9 | {name="Emily Price", email="emily.j.price.nth@gmail.com"},
10 | { name="Jacob Tomlinson"},
11 | ]
12 | description = "Python interface to the Met Office's Datapoint API"
13 | readme = "README.md"
14 | requires-python = ">=3.9"
15 | classifiers=[
16 | "Development Status :: 3 - Alpha",
17 | "Programming Language :: Python :: 3",
18 | "Programming Language :: Python :: 3.9",
19 | "Programming Language :: Python :: 3.10",
20 | "Programming Language :: Python :: 3.11",
21 | "Programming Language :: Python :: 3.12",
22 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
23 | ]
24 | dependencies = [
25 | "requests >= 2.20.0,<3",
26 | "appdirs >=1,<2",
27 | "geojson >= 3.0.0,<4",
28 | ]
29 | license = {file = "LICENSE"}
30 | keywords = ["weather", "weather forecast", "Met Office", "DataHub"]
31 |
32 | [project.urls]
33 | Homepage = "https://github.com/Perseudonymous/datapoint-python"
34 | Documentation = "http://datapoint-python.readthedocs.org/en/latest"
35 |
36 | [tool.hatch.build.targets.sdist]
37 | exclude = [
38 | "tests/",
39 | "examples/",
40 | ]
41 |
42 | [tool.hatch.version]
43 | source = "versioningit"
44 |
45 | [tool.isort]
46 | profile = "black"
47 | src_paths = ["src", "tests"]
48 |
49 | [tool.versioningit.format]
50 |
51 | distance = "{base_version}+post{distance}{vcs}{rev}"
52 | distance-dirty = "{base_version}+post{distance}{vcs}{rev}.d{build_date:%Y%m%d}"
53 |
54 | [tool.versioningit.vcs]
55 | default-tag = "0.0.1"
56 |
57 | [tool.pytest.ini_options]
58 | addopts = [
59 | "--import-mode=importlib",
60 | ]
61 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | black==24.*
2 | isort==5.*
3 | flake8==7.*
4 | flake8-bugbear==24.*
5 | flake8-pytest-style==2.*
6 | pytest==8.*
7 | .
8 |
--------------------------------------------------------------------------------
/src/datapoint/Forecast.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | from datapoint.exceptions import APIException
4 | from datapoint.weather_codes import WEATHER_CODES
5 |
6 |
7 | class Forecast:
8 | """Forecast data returned from DataHub
9 |
10 | Provides access to forecasts as far ahead as provided by DataHub. See the
11 | DataHub documentation for the latest limits on the forecast range. The
12 | values of data from DataHub are combined with the unit information and
13 | description and returned as a dict.
14 |
15 | Basic Usage::
16 |
17 | >>> import datapoint
18 | >>> m = datapoint.Manager.Manager(api_key = "blah")
19 | >>> f = m.get_forecast(
20 | latitude=50,
21 | longitude=0,
22 | frequency="hourly",
23 | convert_weather_code=True,
24 | )
25 | >>> f.now()
26 | {
27 | 'time': datetime.datetime(2024, 2, 19, 13, 0, tzinfo=datetime.timezone.utc),
28 | 'screenTemperature': {
29 | 'value': 10.09,
30 | 'description': 'Screen Air Temperature',
31 | 'unit_name': 'degrees Celsius',
32 | 'unit_symbol': 'Cel'
33 | },
34 | 'screenDewPointTemperature': {
35 | 'value': 8.08,
36 | 'description': 'Screen Dew Point Temperature',
37 | 'unit_name': 'degrees Celsius',
38 | 'unit_symbol': 'Cel'
39 | },
40 | 'feelsLikeTemperature': {
41 | 'value': 6.85,
42 | 'description': 'Feels Like Temperature',
43 | 'unit_name': 'degrees Celsius',
44 | 'unit_symbol': 'Cel'
45 | },
46 | 'windSpeed10m': {
47 | 'value': 7.57,
48 | 'description': '10m Wind Speed',
49 | 'unit_name': 'metres per second',
50 | 'unit_symbol': 'm/s'
51 | },
52 | 'windDirectionFrom10m': {
53 | 'value': 263,
54 | 'description': '10m Wind From Direction',
55 | 'unit_name': 'degrees',
56 | 'unit_symbol': 'deg'
57 | },
58 | 'windGustSpeed10m': {
59 | 'value': 12.31,
60 | 'description': '10m Wind Gust Speed',
61 | 'unit_name': 'metres per second',
62 | 'unit_symbol': 'm/s'
63 | },
64 | 'visibility': {
65 | 'value': 21201,
66 | 'description': 'Visibility',
67 | 'unit_name': 'metres',
68 | 'unit_symbol': 'm'
69 | },
70 | 'screenRelativeHumidity': {
71 | 'value': 87.81,
72 | 'description': 'Screen Relative Humidity',
73 | 'unit_name': 'percentage',
74 | 'unit_symbol': '%'
75 | },
76 | 'mslp': {
77 | 'value': 103080,
78 | 'description': 'Mean Sea Level Pressure',
79 | 'unit_name': 'pascals',
80 | 'unit_symbol': 'Pa'
81 | },
82 | 'uvIndex': {
83 | 'value': 1,
84 | 'description': 'UV Index',
85 | 'unit_name': 'dimensionless',
86 | 'unit_symbol': '1'
87 | },
88 | 'significantWeatherCode': {
89 | 'value': 'Cloudy',
90 | 'description': 'Significant Weather Code',
91 | 'unit_name': 'dimensionless',
92 | 'unit_symbol': '1'
93 | },
94 | 'precipitationRate': {
95 | 'value': 0.0,
96 | 'description': 'Precipitation Rate',
97 | 'unit_name': 'millimetres per hour',
98 | 'unit_symbol': 'mm/h'
99 | },
100 | 'probOfPrecipitation': {
101 | 'value': 21,
102 | 'description': 'Probability of Precipitation',
103 | 'unit_name': 'percentage',
104 | 'unit_symbol': '%'
105 | }
106 | }
107 | """
108 |
109 | def __init__(self, frequency, api_data, convert_weather_code):
110 | """
111 | :param frequency: Frequency of forecast: 'hourly', 'three-hourly',
112 | 'twice-daily', 'daily'
113 | :param api_data: Data returned from API call
114 | :param: convert_weather_code: Convert numeric weather codes to string description
115 | :type frequency: string
116 | :type api_data: dict
117 | :type convert_weather_code: bool
118 | """
119 | self.frequency = frequency
120 | # Need to parse format like 2024-02-17T15:00Z. This can only be
121 | # done with datetime.datetime.fromisoformat from python 3.11
122 | # onwards.
123 | self.data_date = datetime.datetime.strptime(
124 | api_data["features"][0]["properties"]["modelRunDate"],
125 | "%Y-%m-%dT%H:%M%z",
126 | ) #: The date the provided forecast was generated.
127 |
128 | self.forecast_longitude = api_data["features"][0]["geometry"]["coordinates"][
129 | 0
130 | ] #: The longitude of the provided forecast.
131 | self.forecast_latitude = api_data["features"][0]["geometry"]["coordinates"][
132 | 1
133 | ] #: The latitude of the provided forecast.
134 | self.distance_from_requested_location = api_data["features"][0]["properties"][
135 | "requestPointDistance"
136 | ] #: The distance of the location of the provided forecast from the requested location
137 | self.name = api_data["features"][0]["properties"]["location"][
138 | "name"
139 | ] #: The name of the location of the provided forecast
140 |
141 | # N.B. Elevation is in metres above or below the WGS 84 reference
142 | # ellipsoid as per GeoJSON spec.
143 | self.elevation = api_data["features"][0]["geometry"]["coordinates"][
144 | 2
145 | ] #: The elevation of the location of the provided forecast
146 |
147 | self.convert_weather_code = (
148 | convert_weather_code #: Convert numeric weather codes to string description
149 | )
150 |
151 | forecasts = api_data["features"][0]["properties"]["timeSeries"]
152 | parameters = api_data["parameters"][0]
153 | if frequency == "twice-daily":
154 | self.timesteps = self._build_twice_daily_timesteps(forecasts, parameters)
155 | else:
156 | self.timesteps = []
157 | for forecast in forecasts:
158 | self.timesteps.append(self._build_timestep(forecast, parameters))
159 |
160 | def _build_twice_daily_timesteps(self, forecasts, parameters):
161 | """Build individual timesteps from forecasts and metadata
162 |
163 | Take the forecast data from DataHub and combine with unit information
164 | in each timestep. Break each day into day and night steps. ASSUME that
165 | each step has data for the night referred to in the timestamp and the
166 | following dawn-dusk period.
167 |
168 | :parameter forecasts: Forecast data from DataHub
169 | :parameter parameters: Unit information from DataHub
170 | :type forecasts: list
171 | :type parameters: dict
172 |
173 | :return: List of timesteps
174 | :rtype: list
175 | """
176 |
177 | timesteps = []
178 | for forecast in forecasts:
179 | # Need to parse format like 2024-02-17T15:00Z. This can only be
180 | # done with datetime.datetime.fromisoformat from python 3.11
181 | # onwards.
182 | night_step = {
183 | "time": datetime.datetime.strptime(forecast["time"], "%Y-%m-%dT%H:%M%z")
184 | }
185 | day_step = {
186 | "time": datetime.datetime.strptime(forecast["time"], "%Y-%m-%dT%H:%M%z")
187 | + datetime.timedelta(hours=12)
188 | }
189 |
190 | for element, value in forecast.items():
191 | if element.startswith("midday"):
192 | day_step[element] = {
193 | "value": value,
194 | "description": parameters[element]["description"],
195 | "unit_name": parameters[element]["unit"]["label"],
196 | "unit_symbol": parameters[element]["unit"]["symbol"]["type"],
197 | }
198 | elif element.startswith("midnight"):
199 | night_step[element] = {
200 | "value": value,
201 | "description": parameters[element]["description"],
202 | "unit_name": parameters[element]["unit"]["label"],
203 | "unit_symbol": parameters[element]["unit"]["symbol"]["type"],
204 | }
205 | elif element.startswith("day"):
206 | if (
207 | element == "daySignificantWeatherCode"
208 | and self.convert_weather_code
209 | ):
210 | day_step[element] = {
211 | "value": WEATHER_CODES[str(value)],
212 | "description": parameters[element]["description"],
213 | "unit_name": parameters[element]["unit"]["label"],
214 | "unit_symbol": parameters[element]["unit"]["symbol"][
215 | "type"
216 | ],
217 | }
218 |
219 | else:
220 | day_step[element] = {
221 | "value": value,
222 | "description": parameters[element]["description"],
223 | "unit_name": parameters[element]["unit"]["label"],
224 | "unit_symbol": parameters[element]["unit"]["symbol"][
225 | "type"
226 | ],
227 | }
228 | elif element.startswith("night"):
229 | if (
230 | element == "nightSignificantWeatherCode"
231 | and self.convert_weather_code
232 | ):
233 | night_step[element] = {
234 | "value": WEATHER_CODES[str(value)],
235 | "description": parameters[element]["description"],
236 | "unit_name": parameters[element]["unit"]["label"],
237 | "unit_symbol": parameters[element]["unit"]["symbol"][
238 | "type"
239 | ],
240 | }
241 |
242 | else:
243 | night_step[element] = {
244 | "value": value,
245 | "description": parameters[element]["description"],
246 | "unit_name": parameters[element]["unit"]["label"],
247 | "unit_symbol": parameters[element]["unit"]["symbol"][
248 | "type"
249 | ],
250 | }
251 | elif element == "maxUvIndex":
252 | day_step[element] = {
253 | "value": value,
254 | "description": parameters[element]["description"],
255 | "unit_name": parameters[element]["unit"]["label"],
256 | "unit_symbol": parameters[element]["unit"]["symbol"]["type"],
257 | }
258 |
259 | timesteps.append(night_step)
260 | timesteps.append(day_step)
261 |
262 | timesteps = sorted(timesteps, key=lambda t: t["time"])
263 | return timesteps
264 |
265 | def _build_timestep(self, forecast, parameters):
266 | """Build individual timestep from forecast and metadata
267 |
268 | Take the forecast data from DataHub for a single time and combine with
269 | unit information in each timestep.
270 |
271 | :parameter forecast: Forecast data from DataHub
272 | :parameter parameters: Unit information from DataHub
273 | :type forecast: dict
274 | :type parameters:dict
275 |
276 | :return: Individual forecast timestep
277 | :rtype: dict
278 |
279 | """
280 |
281 | timestep = {}
282 | for element, value in forecast.items():
283 | if element == "time":
284 | # Need to parse format like 2024-02-17T15:00Z. This can only be
285 | # done with datetime.datetime.fromisoformat from python 3.11
286 | # onwards.
287 | timestep["time"] = datetime.datetime.strptime(
288 | forecast["time"], "%Y-%m-%dT%H:%M%z"
289 | )
290 |
291 | elif (
292 | element
293 | in (
294 | "significantWeatherCode",
295 | "daySignificantWeatherCode",
296 | "nightSignificantWeatherCode",
297 | )
298 | ) and self.convert_weather_code:
299 | timestep[element] = {
300 | "value": WEATHER_CODES[str(value)],
301 | "description": parameters[element]["description"],
302 | "unit_name": parameters[element]["unit"]["label"],
303 | "unit_symbol": parameters[element]["unit"]["symbol"]["type"],
304 | }
305 | else:
306 | timestep[element] = {
307 | "value": value,
308 | "description": parameters[element]["description"],
309 | "unit_name": parameters[element]["unit"]["label"],
310 | "unit_symbol": parameters[element]["unit"]["symbol"]["type"],
311 | }
312 |
313 | return timestep
314 |
315 | def _check_requested_time(self, target):
316 | """Check that a forecast for the requested time can be provided
317 |
318 | :parameter target: The requested time for the forecast
319 | :type target: datetime
320 | """
321 | # Check that there is a forecast for the requested time.
322 | # If we have an hourly forecast, check that the requested time is at
323 | # most 30 minutes before the first datetime we have a forecast for.
324 | if self.frequency == "hourly" and target < self.timesteps[0][
325 | "time"
326 | ] - datetime.timedelta(hours=0, minutes=30):
327 | err_str = (
328 | "There is no forecast available for the requested time. "
329 | "The requested time is more than 30 minutes before the "
330 | "first available forecast."
331 | )
332 | raise APIException(err_str)
333 |
334 | # If we have a three-hourly forecast, check that the requested time is at
335 | # most 1.5 hours before the first datetime we have a forecast for.
336 | if self.frequency == "three-hourly" and target < self.timesteps[0][
337 | "time"
338 | ] - datetime.timedelta(hours=1, minutes=30):
339 | err_str = (
340 | "There is no forecast available for the requested time. "
341 | "The requested time is more than 1 hour and 30 minutes "
342 | "before the first available forecast."
343 | )
344 | raise APIException(err_str)
345 |
346 | # If we have a daily forecast, check that the requested time is at
347 | # most 6 hours before the first datetime we have a forecast for.
348 | if self.frequency == "daily" and target < self.timesteps[0][
349 | "time"
350 | ] - datetime.timedelta(hours=6):
351 | err_str = (
352 | "There is no forecast available for the requested time. "
353 | "The requested time is more than 6 hours before the first "
354 | "available forecast."
355 | )
356 |
357 | raise APIException(err_str)
358 |
359 | # If we have a twice-daily forecast, check that the requested time is
360 | # at most 6 hours before the first datetime we have a forecast for.
361 | if self.frequency == "twice-daily" and target < self.timesteps[0][
362 | "time"
363 | ] - datetime.timedelta(hours=6):
364 | err_str = (
365 | "There is no forecast available for the requested time. "
366 | "The requested time is more than 6 hours before the first "
367 | "available forecast."
368 | )
369 |
370 | raise APIException(err_str)
371 |
372 | # If we have an hourly forecast, check that the requested time is at
373 | # most 30 minutes after the final datetime we have a forecast for
374 | if self.frequency == "hourly" and target > (
375 | self.timesteps[-1]["time"] + datetime.timedelta(hours=0, minutes=30)
376 | ):
377 | err_str = (
378 | "There is no forecast available for the requested time. The "
379 | "requested time is more than 30 minutes after the first "
380 | "available forecast"
381 | )
382 |
383 | raise APIException(err_str)
384 |
385 | # If we have a three-hourly forecast, then the target must be within 1.5
386 | # hours of the last timestep
387 | if self.frequency == "three-hourly" and target > (
388 | self.timesteps[-1]["time"] + datetime.timedelta(hours=1, minutes=30)
389 | ):
390 | err_str = (
391 | "There is no forecast available for the requested time. The "
392 | "requested time is more than 1.5 hours after the first "
393 | "available forecast."
394 | )
395 |
396 | raise APIException(err_str)
397 |
398 | # If we have a daily forecast, then the target must be within 6 hours
399 | # of the last timestep
400 | if self.frequency == "daily" and target > (
401 | self.timesteps[-1]["time"] + datetime.timedelta(hours=6)
402 | ):
403 | err_str = (
404 | "There is no forecast available for the requested time. The "
405 | "requested time is more than 6 hours after the first available "
406 | "forecast."
407 | )
408 |
409 | raise APIException(err_str)
410 |
411 | # If we have a twice-daily forecast, then the target must be within 6 hours
412 | # of the last timestep
413 | if self.frequency == "twice-daily" and target > (
414 | self.timesteps[-1]["time"] + datetime.timedelta(hours=6)
415 | ):
416 | err_str = (
417 | "There is no forecast available for the requested time. The "
418 | "requested time is more than 6 hours after the first available "
419 | "forecast."
420 | )
421 |
422 | raise APIException(err_str)
423 |
424 | def at_datetime(self, target):
425 | """Return the timestep closest to the target datetime
426 |
427 | :parameter target: Time to get the forecast for
428 | :type target: datetime
429 |
430 | :return: Individual forecast timestep
431 | :rtype: dict
432 |
433 | """
434 |
435 | # Convert target to offset aware datetime
436 | if target.tzinfo is None:
437 | target = datetime.datetime.combine(
438 | target.date(), target.time(), self.timesteps[0]["time"].tzinfo
439 | )
440 |
441 | self._check_requested_time(target)
442 |
443 | # Loop over all timesteps
444 | # Calculate the first time difference
445 | prev_td = target - self.timesteps[0]["time"]
446 | prev_ts = self.timesteps[0]
447 | to_return = None
448 |
449 | for i, timestep in enumerate(self.timesteps, start=1):
450 | # Calculate the difference between the target time and the
451 | # timestep.
452 | td = target - timestep["time"]
453 |
454 | # Find the timestep which is further from the target than the
455 | # previous one. Return the previous timestep
456 | if abs(td.total_seconds()) > abs(prev_td.total_seconds()):
457 | # We are further from the target
458 | to_return = prev_ts
459 | break
460 | if i == len(self.timesteps):
461 | to_return = timestep
462 |
463 | prev_ts = timestep
464 | prev_td = td
465 | return to_return
466 |
467 | def now(self):
468 | """Return the closest timestep to the current time
469 |
470 | :return: Individual forecast timestep
471 | :rtype: dict
472 | """
473 |
474 | d = datetime.datetime.now(tz=self.timesteps[0]["time"].tzinfo)
475 | return self.at_datetime(d)
476 |
477 | def future(self, days=0, hours=0, minutes=0):
478 | """Return the closest timestep to a date in a given amount of time
479 |
480 | Either provide components of the time to the forecast or the total
481 | hours or minutes
482 |
483 | Providing components::
484 |
485 | >>> import datapoint
486 | >>> m = datapoint.Manager(api_key = "blah")
487 | >>> f = m.get_forecast(latitude=50, longitude=0, frequency="hourly")
488 | >>> f.future(days=1, hours=2)
489 |
490 | Providing total hours::
491 |
492 | >>> import datapoint
493 | >>> m = datapoint.Manager(api_key = "blah")
494 | >>> f = m.get_forecast(latitude=50, longitude=0, frequency="hourly")
495 | >>> f.future(hours=26)
496 |
497 |
498 | :parameter days: How many days ahead
499 | :parameter hours: How many hours ahead
500 | :parameter minutes: How many minutes ahead
501 | :type days: int
502 | :type hours: int
503 | :type minutes: int
504 |
505 | :return: Individual forecast timestep
506 | :rtype: dict
507 | """
508 |
509 | d = datetime.datetime.now(tz=self.timesteps[0]["time"].tzinfo)
510 | target = d + datetime.timedelta(days=days, hours=hours, minutes=minutes)
511 |
512 | return self.at_datetime(target)
513 |
--------------------------------------------------------------------------------
/src/datapoint/Manager.py:
--------------------------------------------------------------------------------
1 | import geojson
2 | import requests
3 | from requests.adapters import HTTPAdapter
4 | from requests.packages.urllib3.util.retry import Retry
5 |
6 | from datapoint.exceptions import APIException
7 | from datapoint.Forecast import Forecast
8 |
9 | API_URL = "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/"
10 |
11 |
12 | class Manager:
13 | """Manager for DataHub connection.
14 |
15 | Wraps calls to DataHub API, and provides Forecast objects. Basic Usage:
16 |
17 | ::
18 |
19 | >>> import datapoint
20 | >>> m = datapoint.Manager.Manager(api_key = "blah")
21 | >>> f = m.get_forecast(
22 | latitude=50,
23 | longitude=0,
24 | frequency="hourly",
25 | convert_weather_code=True
26 | )
27 | >>> f.now()
28 | {
29 | 'time': datetime.datetime(2024, 2, 19, 13, 0, tzinfo=datetime.timezone.utc),
30 | 'screenTemperature': {
31 | 'value': 10.09,
32 | 'description': 'Screen Air Temperature',
33 | 'unit_name': 'degrees Celsius',
34 | 'unit_symbol': 'Cel'
35 | },
36 | 'screenDewPointTemperature': {
37 | 'value': 8.08,
38 | 'description': 'Screen Dew Point Temperature',
39 | 'unit_name': 'degrees Celsius',
40 | 'unit_symbol': 'Cel'
41 | },
42 | 'feelsLikeTemperature': {
43 | 'value': 6.85,
44 | 'description': 'Feels Like Temperature',
45 | 'unit_name': 'degrees Celsius',
46 | 'unit_symbol': 'Cel'
47 | },
48 | 'windSpeed10m': {
49 | 'value': 7.57,
50 | 'description': '10m Wind Speed',
51 | 'unit_name': 'metres per second',
52 | 'unit_symbol': 'm/s'
53 | },
54 | 'windDirectionFrom10m': {
55 | 'value': 263,
56 | 'description': '10m Wind From Direction',
57 | 'unit_name': 'degrees',
58 | 'unit_symbol': 'deg'
59 | },
60 | 'windGustSpeed10m': {
61 | 'value': 12.31,
62 | 'description': '10m Wind Gust Speed',
63 | 'unit_name': 'metres per second',
64 | 'unit_symbol': 'm/s'
65 | },
66 | 'visibility': {
67 | 'value': 21201,
68 | 'description': 'Visibility',
69 | 'unit_name': 'metres',
70 | 'unit_symbol': 'm'
71 | },
72 | 'screenRelativeHumidity': {
73 | 'value': 87.81,
74 | 'description': 'Screen Relative Humidity',
75 | 'unit_name': 'percentage',
76 | 'unit_symbol': '%'
77 | },
78 | 'mslp': {
79 | 'value': 103080,
80 | 'description': 'Mean Sea Level Pressure',
81 | 'unit_name': 'pascals',
82 | 'unit_symbol': 'Pa'
83 | },
84 | 'uvIndex': {
85 | 'value': 1,
86 | 'description': 'UV Index',
87 | 'unit_name': 'dimensionless',
88 | 'unit_symbol': '1'
89 | },
90 | 'significantWeatherCode': {
91 | 'value': 'Cloudy',
92 | 'description': 'Significant Weather Code',
93 | 'unit_name': 'dimensionless',
94 | 'unit_symbol': '1'
95 | },
96 | 'precipitationRate': {
97 | 'value': 0.0,
98 | 'description': 'Precipitation Rate',
99 | 'unit_name': 'millimetres per hour',
100 | 'unit_symbol': 'mm/h'
101 | },
102 | 'probOfPrecipitation': {
103 | 'value': 21,
104 | 'description': 'Probability of Precipitation',
105 | 'unit_name': 'percentage',
106 | 'unit_symbol': '%'
107 | }
108 | }
109 |
110 | """
111 |
112 | def __init__(self, api_key=""):
113 | self.api_key = api_key
114 |
115 | def __get_retry_session(
116 | self,
117 | retries=10,
118 | backoff_factor=0.3,
119 | status_forcelist=(500, 502, 504),
120 | session=None,
121 | ):
122 | """
123 | Retry the connection using requests if it fails. Use this as a wrapper
124 | to request from datapoint. See
125 | https://requests.readthedocs.io/en/latest/user/advanced/?highlight=retry#example-automatic-retries
126 | for more details.
127 |
128 | :parameter retries: How many times to retry
129 | :parameter backoff_factor: Backoff between attempts after second try
130 | :parameter status_forcelist: Codes to force a retry on
131 | :parameter session: Existing session to use
132 |
133 | :return: Session object
134 | :rtype:
135 | """
136 |
137 | # requests.Session allows finer control, which is needed to use the
138 | # retrying code
139 | the_session = session or requests.Session()
140 |
141 | # The Retry object manages the actual retrying
142 | retry = Retry(
143 | total=retries,
144 | read=retries,
145 | connect=retries,
146 | backoff_factor=backoff_factor,
147 | status_forcelist=status_forcelist,
148 | )
149 |
150 | adapter = HTTPAdapter(max_retries=retry)
151 |
152 | the_session.mount("http://", adapter)
153 | the_session.mount("https://", adapter)
154 |
155 | return the_session
156 |
157 | def __call_api(self, latitude, longitude, frequency):
158 | """
159 | Call the datapoint api using the requests module
160 |
161 | :parameter latitude: Latitude of forecast location
162 | :parameter longitude: Longitude of forecast location
163 | :parameter frequency: Forecast frequency. One of 'hourly', 'three-hourly, 'daily'
164 | :type latitude: float
165 | :type longitude: float
166 | :type frequency: string
167 |
168 | :return: Data from DataPoint
169 | :rtype: dict
170 | """
171 | params = {
172 | "latitude": latitude,
173 | "longitude": longitude,
174 | "includeLocationName": True,
175 | "excludeParameterMetadata": False,
176 | }
177 | headers = {
178 | "accept": "application/json",
179 | "apikey": self.api_key,
180 | }
181 |
182 | if frequency == "twice-daily":
183 | request_url = API_URL + "daily"
184 | else:
185 | request_url = API_URL + frequency
186 |
187 | # Add a timeout to the request.
188 | # The value of 1 second is based on attempting 100 connections to
189 | # datapoint and taking ten times the mean connection time (rounded up).
190 | # Could expose to users in the functions which need to call the api.
191 | # req = requests.get(url, params=payload, timeout=1)
192 | # The wrapper function __retry_session returns a requests.Session
193 | # object. This has a .get() function like requests.get(), so the use
194 | # doesn't change here.
195 |
196 | sess = self.__get_retry_session()
197 | req = sess.get(
198 | request_url,
199 | params=params,
200 | headers=headers,
201 | timeout=1,
202 | )
203 |
204 | req.raise_for_status()
205 |
206 | try:
207 | data = geojson.loads(req.text)
208 | except ValueError as exc:
209 | raise APIException("DataPoint has not returned valid JSON") from exc
210 |
211 | return data
212 |
213 | def get_forecast(
214 | self, latitude, longitude, frequency="daily", convert_weather_code=True
215 | ):
216 | """
217 | Get a forecast for the provided site. Three frequencies are supported
218 | by DataHub: hourly, three-hourly and daily. The 'twice-daily' option is
219 | for convenience and splits a daily forecast into two steps, one for day
220 | and one for night.
221 |
222 | :parameter latitude: Latitude of forecast location
223 | :parameter longitude: Longitude of forecast location
224 | :parameter frequency: Forecast frequency. One of 'hourly',
225 | 'three-hourly,'twice-daily', 'daily'
226 | :parameter convert_weather_code: Convert numeric weather codes to string description
227 | :type latitude: float
228 | :type longitude: float
229 | :type frequency: string
230 | :type convert_weather_code: bool
231 |
232 | :return: :class: `Forecast ` object
233 | :rtype: datapoint.Forecast
234 | """
235 | if frequency not in ["hourly", "three-hourly", "twice-daily", "daily"]:
236 | raise ValueError(
237 | "frequency must be set to one of 'hourly', 'three-hourly', "
238 | "'twice-daily', 'daily'"
239 | )
240 | data = self.__call_api(latitude, longitude, frequency)
241 | forecast = Forecast(
242 | frequency=frequency,
243 | api_data=data,
244 | convert_weather_code=convert_weather_code,
245 | )
246 |
247 | return forecast
248 |
--------------------------------------------------------------------------------
/src/datapoint/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Perseudonymous/datapoint-python/0261199dc02229fdf0b5b1239431177ecf728c56/src/datapoint/__init__.py
--------------------------------------------------------------------------------
/src/datapoint/exceptions.py:
--------------------------------------------------------------------------------
1 | class APIException(Exception):
2 | """When Datapoint returns a broken API response."""
3 |
4 | pass
5 |
--------------------------------------------------------------------------------
/src/datapoint/weather_codes.py:
--------------------------------------------------------------------------------
1 | # See https://datahub.metoffice.gov.uk/support/faqs for definitions
2 | WEATHER_CODES = {
3 | "NA": "Not available",
4 | "0": "Clear night",
5 | "1": "Sunny day",
6 | "2": "Partly cloudy",
7 | "3": "Partly cloudy",
8 | "4": "Not used",
9 | "5": "Mist",
10 | "6": "Fog",
11 | "7": "Cloudy",
12 | "8": "Overcast",
13 | "9": "Light rain shower",
14 | "10": "Light rain shower",
15 | "11": "Drizzle",
16 | "12": "Light rain",
17 | "13": "Heavy rain shower",
18 | "14": "Heavy rain shower",
19 | "15": "Heavy rain",
20 | "16": "Sleet shower",
21 | "17": "Sleet shower",
22 | "18": "Sleet",
23 | "19": "Hail shower",
24 | "20": "Hail shower",
25 | "21": "Hail",
26 | "22": "Light snow shower",
27 | "23": "Light snow shower",
28 | "24": "Light snow",
29 | "25": "Heavy snow shower",
30 | "26": "Heavy snow shower",
31 | "27": "Heavy snow",
32 | "28": "Thunder shower",
33 | "29": "Thunder shower",
34 | "30": "Thunder",
35 | }
36 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Perseudonymous/datapoint-python/0261199dc02229fdf0b5b1239431177ecf728c56/tests/__init__.py
--------------------------------------------------------------------------------
/tests/integration/test_manager.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import requests
3 |
4 | import tests.reference_data.reference_data_test_forecast as reference_data_test_forecast
5 | from datapoint.Manager import Manager
6 |
7 |
8 | class MockResponseHourly:
9 | def __init__(self):
10 | with open("./tests/reference_data/hourly_api_data.json") as f:
11 | my_json = f.read()
12 |
13 | self.text = my_json
14 |
15 | @staticmethod
16 | def raise_for_status():
17 | pass
18 |
19 |
20 | @pytest.fixture
21 | def _mock_response_hourly(monkeypatch):
22 | def mock_get(*args, **kwargs):
23 | return MockResponseHourly()
24 |
25 | monkeypatch.setattr(requests.Session, "get", mock_get)
26 |
27 |
28 | @pytest.fixture
29 | def hourly_forecast(_mock_response_hourly):
30 | m = Manager(api_key="aaaaaaaaaaaaaaaaaaaaaaaaa")
31 | f = m.get_forecast(50.9992, 0.0154, frequency="hourly", convert_weather_code=True)
32 | return f
33 |
34 |
35 | @pytest.fixture
36 | def expected_first_hourly_timestep():
37 | return reference_data_test_forecast.EXPECTED_FIRST_HOURLY_TIMESTEP
38 |
39 |
40 | class MockResponseThreeHourly:
41 | def __init__(self):
42 | with open("./tests/reference_data/three_hourly_api_data.json") as f:
43 | my_json = f.read()
44 |
45 | self.text = my_json
46 |
47 | @staticmethod
48 | def raise_for_status():
49 | pass
50 |
51 |
52 | @pytest.fixture
53 | def _mock_response_three_hourly(monkeypatch):
54 | def mock_get(*args, **kwargs):
55 | return MockResponseThreeHourly()
56 |
57 | monkeypatch.setattr(requests.Session, "get", mock_get)
58 |
59 |
60 | @pytest.fixture
61 | def three_hourly_forecast(_mock_response_three_hourly):
62 | m = Manager(api_key="aaaaaaaaaaaaaaaaaaaaaaaaa")
63 | f = m.get_forecast(
64 | 50.9992, 0.0154, frequency="three-hourly", convert_weather_code=True
65 | )
66 | return f
67 |
68 |
69 | @pytest.fixture
70 | def expected_first_three_hourly_timestep():
71 | return reference_data_test_forecast.EXPECTED_FIRST_THREE_HOURLY_TIMESTEP
72 |
73 |
74 | class MockResponseDaily:
75 | def __init__(self):
76 | with open("./tests/reference_data/daily_api_data.json") as f:
77 | my_json = f.read()
78 |
79 | self.text = my_json
80 |
81 | @staticmethod
82 | def raise_for_status():
83 | pass
84 |
85 |
86 | @pytest.fixture
87 | def _mock_response_daily(monkeypatch):
88 | def mock_get(*args, **kwargs):
89 | return MockResponseDaily()
90 |
91 | monkeypatch.setattr(requests.Session, "get", mock_get)
92 |
93 |
94 | @pytest.fixture
95 | def daily_forecast(_mock_response_daily):
96 | m = Manager(api_key="aaaaaaaaaaaaaaaaaaaaaaaaa")
97 | f = m.get_forecast(50.9992, 0.0154, frequency="daily", convert_weather_code=True)
98 | return f
99 |
100 |
101 | @pytest.fixture
102 | def twice_daily_forecast(_mock_response_daily):
103 | m = Manager(api_key="aaaaaaaaaaaaaaaaaaaaaaaaa")
104 | f = m.get_forecast(
105 | 50.9992, 0.0154, frequency="twice-daily", convert_weather_code=True
106 | )
107 | return f
108 |
109 |
110 | @pytest.fixture
111 | def expected_first_daily_timestep():
112 | return reference_data_test_forecast.EXPECTED_FIRST_DAILY_TIMESTEP
113 |
114 |
115 | @pytest.fixture
116 | def expected_first_twice_daily_timestep():
117 | return reference_data_test_forecast.EXPECTED_FIRST_TWICE_DAILY_TIMESTEP
118 |
119 |
120 | class TestHourly:
121 | def test_location_name(self, hourly_forecast):
122 | assert hourly_forecast.name == "Sheffield Park"
123 |
124 | def test_forecast_frequency(self, hourly_forecast):
125 | assert hourly_forecast.frequency == "hourly"
126 |
127 | def test_forecast_location_latitude(self, hourly_forecast):
128 | assert hourly_forecast.forecast_latitude == 50.9992
129 |
130 | def test_forecast_location_longitude(self, hourly_forecast):
131 | assert hourly_forecast.forecast_longitude == 0.0154
132 |
133 | def test_forecast_distance_from_request(self, hourly_forecast):
134 | assert hourly_forecast.distance_from_requested_location == 1081.5349
135 |
136 | def test_forecast_elevation(self, hourly_forecast):
137 | assert hourly_forecast.elevation == 37.0
138 |
139 | def test_forecast_first_timestep(
140 | self, hourly_forecast, expected_first_hourly_timestep
141 | ):
142 | assert hourly_forecast.timesteps[0] == expected_first_hourly_timestep
143 |
144 |
145 | class TestThreeHourly:
146 | def test_forecast_frequency(self, three_hourly_forecast):
147 | assert three_hourly_forecast.frequency == "three-hourly"
148 |
149 | def test_forecast_location_name(self, three_hourly_forecast):
150 | assert three_hourly_forecast.name == "Sheffield Park"
151 |
152 | def test_forecast_location_latitude(self, three_hourly_forecast):
153 | assert three_hourly_forecast.forecast_latitude == 50.9992
154 |
155 | def test_forecast_location_longitude(self, three_hourly_forecast):
156 | assert three_hourly_forecast.forecast_longitude == 0.0154
157 |
158 | def test_forecast_distance_from_request(self, three_hourly_forecast):
159 | assert three_hourly_forecast.distance_from_requested_location == 1081.5349
160 |
161 | def test_forecast_elevation(self, three_hourly_forecast):
162 | assert three_hourly_forecast.elevation == 37.0
163 |
164 | def test_forecast_first_timestep(
165 | self, three_hourly_forecast, expected_first_three_hourly_timestep
166 | ):
167 | assert (
168 | three_hourly_forecast.timesteps[0] == expected_first_three_hourly_timestep
169 | )
170 |
171 |
172 | class TestDaily:
173 | def test_forecast_frequency(self, daily_forecast):
174 | assert daily_forecast.frequency == "daily"
175 |
176 | def test_forecast_location_name(self, daily_forecast):
177 | assert daily_forecast.name == "Sheffield Park"
178 |
179 | def test_forecast_location_latitude(self, daily_forecast):
180 | assert daily_forecast.forecast_latitude == 50.9992
181 |
182 | def test_forecast_location_longitude(self, daily_forecast):
183 | assert daily_forecast.forecast_longitude == 0.0154
184 |
185 | def test_forecast_distance_from_request(self, daily_forecast):
186 | assert daily_forecast.distance_from_requested_location == 1081.5349
187 |
188 | def test_forecast_elevation(self, daily_forecast):
189 | assert daily_forecast.elevation == 37.0
190 |
191 | def test_forecast_first_timestep(
192 | self, daily_forecast, expected_first_daily_timestep
193 | ):
194 | assert daily_forecast.timesteps[0] == expected_first_daily_timestep
195 |
196 |
197 | class TestTwiceDaily:
198 | def test_forecast_frequency(self, twice_daily_forecast):
199 | assert twice_daily_forecast.frequency == "twice-daily"
200 |
201 | def test_forecast_location_name(self, twice_daily_forecast):
202 | assert twice_daily_forecast.name == "Sheffield Park"
203 |
204 | def test_forecast_location_latitude(self, twice_daily_forecast):
205 | assert twice_daily_forecast.forecast_latitude == 50.9992
206 |
207 | def test_forecast_location_longitude(self, twice_daily_forecast):
208 | assert twice_daily_forecast.forecast_longitude == 0.0154
209 |
210 | def test_forecast_distance_from_request(self, twice_daily_forecast):
211 | assert twice_daily_forecast.distance_from_requested_location == 1081.5349
212 |
213 | def test_forecast_elevation(self, twice_daily_forecast):
214 | assert twice_daily_forecast.elevation == 37.0
215 |
216 | def test_forecast_first_timestep(
217 | self, twice_daily_forecast, expected_first_twice_daily_timestep
218 | ):
219 | assert twice_daily_forecast.timesteps[0] == expected_first_twice_daily_timestep
220 |
--------------------------------------------------------------------------------
/tests/reference_data/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Perseudonymous/datapoint-python/0261199dc02229fdf0b5b1239431177ecf728c56/tests/reference_data/__init__.py
--------------------------------------------------------------------------------
/tests/reference_data/daily_api_data.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "FeatureCollection",
3 | "parameters": [{
4 | "daySignificantWeatherCode": {
5 | "type": "Parameter",
6 | "description": "Day Significant Weather Code",
7 | "unit": {
8 | "label": "dimensionless",
9 | "symbol": {
10 | "value": "https://metoffice.apiconnect.ibmcloud.com/metoffice/production/",
11 | "type": "1"
12 | }
13 | }
14 | },
15 | "midnightRelativeHumidity": {
16 | "type": "Parameter",
17 | "description": "Relative Humidity at Local Midnight",
18 | "unit": {
19 | "label": "percentage",
20 | "symbol": {
21 | "value": "http://www.opengis.net/def/uom/UCUM/",
22 | "type": "%"
23 | }
24 | }
25 | },
26 | "nightProbabilityOfHeavyRain": {
27 | "type": "Parameter",
28 | "description": "Probability of Heavy Rain During The Night",
29 | "unit": {
30 | "label": "percentage",
31 | "symbol": {
32 | "value": "http://www.opengis.net/def/uom/UCUM/",
33 | "type": "%"
34 | }
35 | }
36 | },
37 | "midnight10MWindSpeed": {
38 | "type": "Parameter",
39 | "description": "10m Wind Speed at Local Midnight",
40 | "unit": {
41 | "label": "metres per second",
42 | "symbol": {
43 | "value": "http://www.opengis.net/def/uom/UCUM/",
44 | "type": "m/s"
45 | }
46 | }
47 | },
48 | "nightUpperBoundMinFeelsLikeTemp": {
49 | "type": "Parameter",
50 | "description": "Upper Bound on Night Minimum Feels Like Air Temperature",
51 | "unit": {
52 | "label": "degrees Celsius",
53 | "symbol": {
54 | "value": "http://www.opengis.net/def/uom/UCUM/",
55 | "type": "Cel"
56 | }
57 | }
58 | },
59 | "nightUpperBoundMinTemp": {
60 | "type": "Parameter",
61 | "description": "Upper Bound on Night Minimum Screen Air Temperature",
62 | "unit": {
63 | "label": "degrees Celsius",
64 | "symbol": {
65 | "value": "http://www.opengis.net/def/uom/UCUM/",
66 | "type": "Cel"
67 | }
68 | }
69 | },
70 | "midnightVisibility": {
71 | "type": "Parameter",
72 | "description": "Visibility at Local Midnight",
73 | "unit": {
74 | "label": "metres",
75 | "symbol": {
76 | "value": "http://www.opengis.net/def/uom/UCUM/",
77 | "type": "m"
78 | }
79 | }
80 | },
81 | "dayUpperBoundMaxFeelsLikeTemp": {
82 | "type": "Parameter",
83 | "description": "Upper Bound on Day Maximum Feels Like Air Temperature",
84 | "unit": {
85 | "label": "degrees Celsius",
86 | "symbol": {
87 | "value": "http://www.opengis.net/def/uom/UCUM/",
88 | "type": "Cel"
89 | }
90 | }
91 | },
92 | "nightProbabilityOfRain": {
93 | "type": "Parameter",
94 | "description": "Probability of Rain During The Night",
95 | "unit": {
96 | "label": "percentage",
97 | "symbol": {
98 | "value": "http://www.opengis.net/def/uom/UCUM/",
99 | "type": "%"
100 | }
101 | }
102 | },
103 | "midday10MWindDirection": {
104 | "type": "Parameter",
105 | "description": "10m Wind Direction at Local Midday",
106 | "unit": {
107 | "label": "degrees",
108 | "symbol": {
109 | "value": "http://www.opengis.net/def/uom/UCUM/",
110 | "type": "deg"
111 | }
112 | }
113 | },
114 | "nightLowerBoundMinFeelsLikeTemp": {
115 | "type": "Parameter",
116 | "description": "Lower Bound on Night Minimum Feels Like Air Temperature",
117 | "unit": {
118 | "label": "degrees Celsius",
119 | "symbol": {
120 | "value": "http://www.opengis.net/def/uom/UCUM/",
121 | "type": "Cel"
122 | }
123 | }
124 | },
125 | "nightProbabilityOfHail": {
126 | "type": "Parameter",
127 | "description": "Probability of Hail During The Night",
128 | "unit": {
129 | "label": "percentage",
130 | "symbol": {
131 | "value": "http://www.opengis.net/def/uom/UCUM/",
132 | "type": "%"
133 | }
134 | }
135 | },
136 | "middayMslp": {
137 | "type": "Parameter",
138 | "description": "Mean Sea Level Pressure at Local Midday",
139 | "unit": {
140 | "label": "pascals",
141 | "symbol": {
142 | "value": "http://www.opengis.net/def/uom/UCUM/",
143 | "type": "Pa"
144 | }
145 | }
146 | },
147 | "dayProbabilityOfHeavySnow": {
148 | "type": "Parameter",
149 | "description": "Probability of Heavy Snow During The Day",
150 | "unit": {
151 | "label": "percentage",
152 | "symbol": {
153 | "value": "http://www.opengis.net/def/uom/UCUM/",
154 | "type": "%"
155 | }
156 | }
157 | },
158 | "nightProbabilityOfPrecipitation": {
159 | "type": "Parameter",
160 | "description": "Probability of Precipitation During The Night",
161 | "unit": {
162 | "label": "percentage",
163 | "symbol": {
164 | "value": "http://www.opengis.net/def/uom/UCUM/",
165 | "type": "%"
166 | }
167 | }
168 | },
169 | "dayProbabilityOfHail": {
170 | "type": "Parameter",
171 | "description": "Probability of Hail During The Day",
172 | "unit": {
173 | "label": "percentage",
174 | "symbol": {
175 | "value": "http://www.opengis.net/def/uom/UCUM/",
176 | "type": "%"
177 | }
178 | }
179 | },
180 | "dayProbabilityOfRain": {
181 | "type": "Parameter",
182 | "description": "Probability of Rain During The Day",
183 | "unit": {
184 | "label": "percentage",
185 | "symbol": {
186 | "value": "http://www.opengis.net/def/uom/UCUM/",
187 | "type": "%"
188 | }
189 | }
190 | },
191 | "midday10MWindSpeed": {
192 | "type": "Parameter",
193 | "description": "10m Wind Speed at Local Midday",
194 | "unit": {
195 | "label": "metres per second",
196 | "symbol": {
197 | "value": "http://www.opengis.net/def/uom/UCUM/",
198 | "type": "m/s"
199 | }
200 | }
201 | },
202 | "midday10MWindGust": {
203 | "type": "Parameter",
204 | "description": "10m Wind Gust Speed at Local Midday",
205 | "unit": {
206 | "label": "metres per second",
207 | "symbol": {
208 | "value": "http://www.opengis.net/def/uom/UCUM/",
209 | "type": "m/s"
210 | }
211 | }
212 | },
213 | "middayVisibility": {
214 | "type": "Parameter",
215 | "description": "Visibility at Local Midday",
216 | "unit": {
217 | "label": "metres",
218 | "symbol": {
219 | "value": "http://www.opengis.net/def/uom/UCUM/",
220 | "type": "m"
221 | }
222 | }
223 | },
224 | "midnight10MWindGust": {
225 | "type": "Parameter",
226 | "description": "10m Wind Gust Speed at Local Midnight",
227 | "unit": {
228 | "label": "metres per second",
229 | "symbol": {
230 | "value": "http://www.opengis.net/def/uom/UCUM/",
231 | "type": "m/s"
232 | }
233 | }
234 | },
235 | "midnightMslp": {
236 | "type": "Parameter",
237 | "description": "Mean Sea Level Pressure at Local Midnight",
238 | "unit": {
239 | "label": "pascals",
240 | "symbol": {
241 | "value": "http://www.opengis.net/def/uom/UCUM/",
242 | "type": "Pa"
243 | }
244 | }
245 | },
246 | "dayProbabilityOfSferics": {
247 | "type": "Parameter",
248 | "description": "Probability of Sferics During The Day",
249 | "unit": {
250 | "label": "percentage",
251 | "symbol": {
252 | "value": "http://www.opengis.net/def/uom/UCUM/",
253 | "type": "%"
254 | }
255 | }
256 | },
257 | "nightSignificantWeatherCode": {
258 | "type": "Parameter",
259 | "description": "Night Significant Weather Code",
260 | "unit": {
261 | "label": "dimensionless",
262 | "symbol": {
263 | "value": "https://metoffice.apiconnect.ibmcloud.com/metoffice/production/",
264 | "type": "1"
265 | }
266 | }
267 | },
268 | "dayProbabilityOfPrecipitation": {
269 | "type": "Parameter",
270 | "description": "Probability of Precipitation During The Day",
271 | "unit": {
272 | "label": "percentage",
273 | "symbol": {
274 | "value": "http://www.opengis.net/def/uom/UCUM/",
275 | "type": "%"
276 | }
277 | }
278 | },
279 | "dayProbabilityOfHeavyRain": {
280 | "type": "Parameter",
281 | "description": "Probability of Heavy Rain During The Day",
282 | "unit": {
283 | "label": "percentage",
284 | "symbol": {
285 | "value": "http://www.opengis.net/def/uom/UCUM/",
286 | "type": "%"
287 | }
288 | }
289 | },
290 | "dayMaxScreenTemperature": {
291 | "type": "Parameter",
292 | "description": "Day Maximum Screen Air Temperature",
293 | "unit": {
294 | "label": "degrees Celsius",
295 | "symbol": {
296 | "value": "http://www.opengis.net/def/uom/UCUM/",
297 | "type": "Cel"
298 | }
299 | }
300 | },
301 | "nightMinScreenTemperature": {
302 | "type": "Parameter",
303 | "description": "Night Minimum Screen Air Temperature",
304 | "unit": {
305 | "label": "degrees Celsius",
306 | "symbol": {
307 | "value": "http://www.opengis.net/def/uom/UCUM/",
308 | "type": "Cel"
309 | }
310 | }
311 | },
312 | "midnight10MWindDirection": {
313 | "type": "Parameter",
314 | "description": "10m Wind Direction at Local Midnight",
315 | "unit": {
316 | "label": "degrees",
317 | "symbol": {
318 | "value": "http://www.opengis.net/def/uom/UCUM/",
319 | "type": "deg"
320 | }
321 | }
322 | },
323 | "maxUvIndex": {
324 | "type": "Parameter",
325 | "description": "Day Maximum UV Index",
326 | "unit": {
327 | "label": "dimensionless",
328 | "symbol": {
329 | "value": "http://www.opengis.net/def/uom/UCUM/",
330 | "type": "1"
331 | }
332 | }
333 | },
334 | "dayProbabilityOfSnow": {
335 | "type": "Parameter",
336 | "description": "Probability of Snow During The Day",
337 | "unit": {
338 | "label": "percentage",
339 | "symbol": {
340 | "value": "http://www.opengis.net/def/uom/UCUM/",
341 | "type": "%"
342 | }
343 | }
344 | },
345 | "nightProbabilityOfSnow": {
346 | "type": "Parameter",
347 | "description": "Probability of Snow During The Night",
348 | "unit": {
349 | "label": "percentage",
350 | "symbol": {
351 | "value": "http://www.opengis.net/def/uom/UCUM/",
352 | "type": "%"
353 | }
354 | }
355 | },
356 | "dayLowerBoundMaxTemp": {
357 | "type": "Parameter",
358 | "description": "Lower Bound on Day Maximum Screen Air Temperature",
359 | "unit": {
360 | "label": "degrees Celsius",
361 | "symbol": {
362 | "value": "http://www.opengis.net/def/uom/UCUM/",
363 | "type": "Cel"
364 | }
365 | }
366 | },
367 | "nightProbabilityOfHeavySnow": {
368 | "type": "Parameter",
369 | "description": "Probability of Heavy Snow During The Night",
370 | "unit": {
371 | "label": "percentage",
372 | "symbol": {
373 | "value": "http://www.opengis.net/def/uom/UCUM/",
374 | "type": "%"
375 | }
376 | }
377 | },
378 | "dayLowerBoundMaxFeelsLikeTemp": {
379 | "type": "Parameter",
380 | "description": "Lower Bound on Day Maximum Feels Like Air Temperature",
381 | "unit": {
382 | "label": "degrees Celsius",
383 | "symbol": {
384 | "value": "http://www.opengis.net/def/uom/UCUM/",
385 | "type": "Cel"
386 | }
387 | }
388 | },
389 | "dayUpperBoundMaxTemp": {
390 | "type": "Parameter",
391 | "description": "Upper Bound on Day Maximum Screen Air Temperature",
392 | "unit": {
393 | "label": "degrees Celsius",
394 | "symbol": {
395 | "value": "http://www.opengis.net/def/uom/UCUM/",
396 | "type": "Cel"
397 | }
398 | }
399 | },
400 | "dayMaxFeelsLikeTemp": {
401 | "type": "Parameter",
402 | "description": "Day Maximum Feels Like Air Temperature",
403 | "unit": {
404 | "label": "degrees Celsius",
405 | "symbol": {
406 | "value": "http://www.opengis.net/def/uom/UCUM/",
407 | "type": "Cel"
408 | }
409 | }
410 | },
411 | "middayRelativeHumidity": {
412 | "type": "Parameter",
413 | "description": "Relative Humidity at Local Midday",
414 | "unit": {
415 | "label": "percentage",
416 | "symbol": {
417 | "value": "http://www.opengis.net/def/uom/UCUM/",
418 | "type": "%"
419 | }
420 | }
421 | },
422 | "nightLowerBoundMinTemp": {
423 | "type": "Parameter",
424 | "description": "Lower Bound on Night Minimum Screen Air Temperature",
425 | "unit": {
426 | "label": "degrees Celsius",
427 | "symbol": {
428 | "value": "http://www.opengis.net/def/uom/UCUM/",
429 | "type": "Cel"
430 | }
431 | }
432 | },
433 | "nightMinFeelsLikeTemp": {
434 | "type": "Parameter",
435 | "description": "Night Minimum Feels Like Air Temperature",
436 | "unit": {
437 | "label": "degrees Celsius",
438 | "symbol": {
439 | "value": "http://www.opengis.net/def/uom/UCUM/",
440 | "type": "Cel"
441 | }
442 | }
443 | },
444 | "nightProbabilityOfSferics": {
445 | "type": "Parameter",
446 | "description": "Probability of Sferics During The Night",
447 | "unit": {
448 | "label": "percentage",
449 | "symbol": {
450 | "value": "http://www.opengis.net/def/uom/UCUM/",
451 | "type": "%"
452 | }
453 | }
454 | }
455 | }],
456 | "features": [{
457 | "type": "Feature",
458 | "geometry": {
459 | "type": "Point",
460 | "coordinates": [0.0154, 50.9992, 37.0]
461 | },
462 | "properties": {
463 | "location": {
464 | "name": "Sheffield Park"
465 | },
466 | "requestPointDistance": 1081.5349,
467 | "modelRunDate": "2024-02-17T14:00Z",
468 | "timeSeries": [{
469 | "time": "2024-02-16T00:00Z",
470 | "midday10MWindSpeed": 5.04,
471 | "midnight10MWindSpeed": 1.39,
472 | "midday10MWindDirection": 273,
473 | "midnight10MWindDirection": 243,
474 | "midday10MWindGust": 8.75,
475 | "midnight10MWindGust": 7.2,
476 | "middayVisibility": 28772,
477 | "midnightVisibility": 27712,
478 | "middayRelativeHumidity": 75.21,
479 | "midnightRelativeHumidity": 80.91,
480 | "middayMslp": 101680,
481 | "midnightMslp": 102640,
482 | "nightSignificantWeatherCode": 7,
483 | "dayMaxScreenTemperature": 12.82,
484 | "nightMinScreenTemperature": 5.32,
485 | "dayUpperBoundMaxTemp": 14.1,
486 | "nightUpperBoundMinTemp": 9.17,
487 | "dayLowerBoundMaxTemp": 11.97,
488 | "nightLowerBoundMinTemp": 3.56,
489 | "nightMinFeelsLikeTemp": 6.27,
490 | "dayUpperBoundMaxFeelsLikeTemp": 12.47,
491 | "nightUpperBoundMinFeelsLikeTemp": 8.74,
492 | "dayLowerBoundMaxFeelsLikeTemp": 10.01,
493 | "nightLowerBoundMinFeelsLikeTemp": 2.75,
494 | "nightProbabilityOfPrecipitation": 11,
495 | "nightProbabilityOfSnow": 0,
496 | "nightProbabilityOfHeavySnow": 0,
497 | "nightProbabilityOfRain": 10,
498 | "nightProbabilityOfHeavyRain": 0,
499 | "nightProbabilityOfHail": 0,
500 | "nightProbabilityOfSferics": 0
501 | }, {
502 | "time": "2024-02-17T00:00Z",
503 | "midday10MWindSpeed": 4.32,
504 | "midnight10MWindSpeed": 6.1,
505 | "midday10MWindDirection": 230,
506 | "midnight10MWindDirection": 218,
507 | "midday10MWindGust": 8.75,
508 | "midnight10MWindGust": 12.98,
509 | "middayVisibility": 4158,
510 | "midnightVisibility": 5915,
511 | "middayRelativeHumidity": 97.38,
512 | "midnightRelativeHumidity": 93.62,
513 | "middayMslp": 103140,
514 | "midnightMslp": 102800,
515 | "maxUvIndex": 1,
516 | "daySignificantWeatherCode": 8,
517 | "nightSignificantWeatherCode": 15,
518 | "dayMaxScreenTemperature": 12.0,
519 | "nightMinScreenTemperature": 9.96,
520 | "dayUpperBoundMaxTemp": 13.71,
521 | "nightUpperBoundMinTemp": 10.71,
522 | "dayLowerBoundMaxTemp": 10.23,
523 | "nightLowerBoundMinTemp": 9.04,
524 | "dayMaxFeelsLikeTemp": 10.6,
525 | "nightMinFeelsLikeTemp": 7.76,
526 | "dayUpperBoundMaxFeelsLikeTemp": 11.49,
527 | "nightUpperBoundMinFeelsLikeTemp": 8.28,
528 | "dayLowerBoundMaxFeelsLikeTemp": 8.38,
529 | "nightLowerBoundMinFeelsLikeTemp": 7.04,
530 | "dayProbabilityOfPrecipitation": 18,
531 | "nightProbabilityOfPrecipitation": 97,
532 | "dayProbabilityOfSnow": 0,
533 | "nightProbabilityOfSnow": 0,
534 | "dayProbabilityOfHeavySnow": 0,
535 | "nightProbabilityOfHeavySnow": 0,
536 | "dayProbabilityOfRain": 18,
537 | "nightProbabilityOfRain": 97,
538 | "dayProbabilityOfHeavyRain": 5,
539 | "nightProbabilityOfHeavyRain": 96,
540 | "dayProbabilityOfHail": 0,
541 | "nightProbabilityOfHail": 20,
542 | "dayProbabilityOfSferics": 0,
543 | "nightProbabilityOfSferics": 10
544 | }, {
545 | "time": "2024-02-18T00:00Z",
546 | "midday10MWindSpeed": 4.07,
547 | "midnight10MWindSpeed": 2.84,
548 | "midday10MWindDirection": 292,
549 | "midnight10MWindDirection": 281,
550 | "midday10MWindGust": 7.55,
551 | "midnight10MWindGust": 6.36,
552 | "middayVisibility": 25425,
553 | "midnightVisibility": 17183,
554 | "middayRelativeHumidity": 86.19,
555 | "midnightRelativeHumidity": 94.68,
556 | "middayMslp": 102625,
557 | "midnightMslp": 103041,
558 | "maxUvIndex": 1,
559 | "daySignificantWeatherCode": 12,
560 | "nightSignificantWeatherCode": 7,
561 | "dayMaxScreenTemperature": 13.89,
562 | "nightMinScreenTemperature": 7.15,
563 | "dayUpperBoundMaxTemp": 14.73,
564 | "nightUpperBoundMinTemp": 9.16,
565 | "dayLowerBoundMaxTemp": 12.4,
566 | "nightLowerBoundMinTemp": 5.31,
567 | "dayMaxFeelsLikeTemp": 11.75,
568 | "nightMinFeelsLikeTemp": 5.34,
569 | "dayUpperBoundMaxFeelsLikeTemp": 13.47,
570 | "nightUpperBoundMinFeelsLikeTemp": 7.12,
571 | "dayLowerBoundMaxFeelsLikeTemp": 10.8,
572 | "nightLowerBoundMinFeelsLikeTemp": 5.06,
573 | "dayProbabilityOfPrecipitation": 55,
574 | "nightProbabilityOfPrecipitation": 9,
575 | "dayProbabilityOfSnow": 0,
576 | "nightProbabilityOfSnow": 0,
577 | "dayProbabilityOfHeavySnow": 0,
578 | "nightProbabilityOfHeavySnow": 0,
579 | "dayProbabilityOfRain": 55,
580 | "nightProbabilityOfRain": 9,
581 | "dayProbabilityOfHeavyRain": 36,
582 | "nightProbabilityOfHeavyRain": 2,
583 | "dayProbabilityOfHail": 4,
584 | "nightProbabilityOfHail": 0,
585 | "dayProbabilityOfSferics": 3,
586 | "nightProbabilityOfSferics": 0
587 | }, {
588 | "time": "2024-02-19T00:00Z",
589 | "midday10MWindSpeed": 5.47,
590 | "midnight10MWindSpeed": 2.59,
591 | "midday10MWindDirection": 276,
592 | "midnight10MWindDirection": 296,
593 | "midday10MWindGust": 10.47,
594 | "midnight10MWindGust": 4.49,
595 | "middayVisibility": 22511,
596 | "midnightVisibility": 23913,
597 | "middayRelativeHumidity": 82.45,
598 | "midnightRelativeHumidity": 89.64,
599 | "middayMslp": 102910,
600 | "midnightMslp": 103121,
601 | "maxUvIndex": 1,
602 | "daySignificantWeatherCode": 3,
603 | "nightSignificantWeatherCode": 7,
604 | "dayMaxScreenTemperature": 12.12,
605 | "nightMinScreenTemperature": 4.02,
606 | "dayUpperBoundMaxTemp": 13.27,
607 | "nightUpperBoundMinTemp": 8.61,
608 | "dayLowerBoundMaxTemp": 10.18,
609 | "nightLowerBoundMinTemp": 1.52,
610 | "dayMaxFeelsLikeTemp": 9.55,
611 | "nightMinFeelsLikeTemp": 2.36,
612 | "dayUpperBoundMaxFeelsLikeTemp": 11.33,
613 | "nightUpperBoundMinFeelsLikeTemp": 6.08,
614 | "dayLowerBoundMaxFeelsLikeTemp": 7.8,
615 | "nightLowerBoundMinFeelsLikeTemp": 1.06,
616 | "dayProbabilityOfPrecipitation": 49,
617 | "nightProbabilityOfPrecipitation": 5,
618 | "dayProbabilityOfSnow": 0,
619 | "nightProbabilityOfSnow": 1,
620 | "dayProbabilityOfHeavySnow": 0,
621 | "nightProbabilityOfHeavySnow": 0,
622 | "dayProbabilityOfRain": 49,
623 | "nightProbabilityOfRain": 5,
624 | "dayProbabilityOfHeavyRain": 30,
625 | "nightProbabilityOfHeavyRain": 0,
626 | "dayProbabilityOfHail": 3,
627 | "nightProbabilityOfHail": 0,
628 | "dayProbabilityOfSferics": 4,
629 | "nightProbabilityOfSferics": 0
630 | }, {
631 | "time": "2024-02-20T00:00Z",
632 | "midday10MWindSpeed": 7.16,
633 | "midnight10MWindSpeed": 6.4,
634 | "midday10MWindDirection": 230,
635 | "midnight10MWindDirection": 232,
636 | "midday10MWindGust": 13.73,
637 | "midnight10MWindGust": 11.87,
638 | "middayVisibility": 24484,
639 | "midnightVisibility": 15050,
640 | "middayRelativeHumidity": 81.96,
641 | "midnightRelativeHumidity": 92.53,
642 | "middayMslp": 102756,
643 | "midnightMslp": 102018,
644 | "maxUvIndex": 1,
645 | "daySignificantWeatherCode": 7,
646 | "nightSignificantWeatherCode": 12,
647 | "dayMaxScreenTemperature": 10.71,
648 | "nightMinScreenTemperature": 8.75,
649 | "dayUpperBoundMaxTemp": 12.07,
650 | "nightUpperBoundMinTemp": 10.16,
651 | "dayLowerBoundMaxTemp": 9.37,
652 | "nightLowerBoundMinTemp": 5.87,
653 | "dayMaxFeelsLikeTemp": 7.33,
654 | "nightMinFeelsLikeTemp": 6.1,
655 | "dayUpperBoundMaxFeelsLikeTemp": 8.37,
656 | "nightUpperBoundMinFeelsLikeTemp": 7.54,
657 | "dayLowerBoundMaxFeelsLikeTemp": 6.38,
658 | "nightLowerBoundMinFeelsLikeTemp": 4.52,
659 | "dayProbabilityOfPrecipitation": 13,
660 | "nightProbabilityOfPrecipitation": 84,
661 | "dayProbabilityOfSnow": 0,
662 | "nightProbabilityOfSnow": 0,
663 | "dayProbabilityOfHeavySnow": 0,
664 | "nightProbabilityOfHeavySnow": 0,
665 | "dayProbabilityOfRain": 13,
666 | "nightProbabilityOfRain": 84,
667 | "dayProbabilityOfHeavyRain": 3,
668 | "nightProbabilityOfHeavyRain": 79,
669 | "dayProbabilityOfHail": 0,
670 | "nightProbabilityOfHail": 15,
671 | "dayProbabilityOfSferics": 0,
672 | "nightProbabilityOfSferics": 8
673 | }, {
674 | "time": "2024-02-21T00:00Z",
675 | "midday10MWindSpeed": 8.38,
676 | "midnight10MWindSpeed": 6.06,
677 | "midday10MWindDirection": 206,
678 | "midnight10MWindDirection": 228,
679 | "midday10MWindGust": 15.83,
680 | "midnight10MWindGust": 11.11,
681 | "middayVisibility": 7351,
682 | "midnightVisibility": 11329,
683 | "middayRelativeHumidity": 90.56,
684 | "midnightRelativeHumidity": 93.31,
685 | "middayMslp": 100933,
686 | "midnightMslp": 99967,
687 | "maxUvIndex": 1,
688 | "daySignificantWeatherCode": 15,
689 | "nightSignificantWeatherCode": 12,
690 | "dayMaxScreenTemperature": 10.79,
691 | "nightMinScreenTemperature": 8.58,
692 | "dayUpperBoundMaxTemp": 13.6,
693 | "nightUpperBoundMinTemp": 11.01,
694 | "dayLowerBoundMaxTemp": 9.05,
695 | "nightLowerBoundMinTemp": 4.97,
696 | "dayMaxFeelsLikeTemp": 6.95,
697 | "nightMinFeelsLikeTemp": 6.72,
698 | "dayUpperBoundMaxFeelsLikeTemp": 10.54,
699 | "nightUpperBoundMinFeelsLikeTemp": 7.88,
700 | "dayLowerBoundMaxFeelsLikeTemp": 5.69,
701 | "nightLowerBoundMinFeelsLikeTemp": 1.79,
702 | "dayProbabilityOfPrecipitation": 86,
703 | "nightProbabilityOfPrecipitation": 78,
704 | "dayProbabilityOfSnow": 0,
705 | "nightProbabilityOfSnow": 0,
706 | "dayProbabilityOfHeavySnow": 0,
707 | "nightProbabilityOfHeavySnow": 0,
708 | "dayProbabilityOfRain": 86,
709 | "nightProbabilityOfRain": 78,
710 | "dayProbabilityOfHeavyRain": 82,
711 | "nightProbabilityOfHeavyRain": 73,
712 | "dayProbabilityOfHail": 16,
713 | "nightProbabilityOfHail": 14,
714 | "dayProbabilityOfSferics": 8,
715 | "nightProbabilityOfSferics": 7
716 | }, {
717 | "time": "2024-02-22T00:00Z",
718 | "midday10MWindSpeed": 8.32,
719 | "midnight10MWindSpeed": 6.61,
720 | "midday10MWindDirection": 215,
721 | "midnight10MWindDirection": 245,
722 | "midday10MWindGust": 15.92,
723 | "midnight10MWindGust": 11.58,
724 | "middayVisibility": 13595,
725 | "midnightVisibility": 25279,
726 | "middayRelativeHumidity": 87.24,
727 | "midnightRelativeHumidity": 77.46,
728 | "middayMslp": 98762,
729 | "midnightMslp": 98742,
730 | "maxUvIndex": 1,
731 | "daySignificantWeatherCode": 14,
732 | "nightSignificantWeatherCode": 2,
733 | "dayMaxScreenTemperature": 10.99,
734 | "nightMinScreenTemperature": 4.47,
735 | "dayUpperBoundMaxTemp": 12.73,
736 | "nightUpperBoundMinTemp": 6.6,
737 | "dayLowerBoundMaxTemp": 8.4,
738 | "nightLowerBoundMinTemp": 2.22,
739 | "dayMaxFeelsLikeTemp": 6.86,
740 | "nightMinFeelsLikeTemp": 1.2,
741 | "dayUpperBoundMaxFeelsLikeTemp": 9.46,
742 | "nightUpperBoundMinFeelsLikeTemp": 2.54,
743 | "dayLowerBoundMaxFeelsLikeTemp": 5.2,
744 | "nightLowerBoundMinFeelsLikeTemp": -0.95,
745 | "dayProbabilityOfPrecipitation": 71,
746 | "nightProbabilityOfPrecipitation": 44,
747 | "dayProbabilityOfSnow": 0,
748 | "nightProbabilityOfSnow": 1,
749 | "dayProbabilityOfHeavySnow": 0,
750 | "nightProbabilityOfHeavySnow": 0,
751 | "dayProbabilityOfRain": 71,
752 | "nightProbabilityOfRain": 44,
753 | "dayProbabilityOfHeavyRain": 67,
754 | "nightProbabilityOfHeavyRain": 32,
755 | "dayProbabilityOfHail": 13,
756 | "nightProbabilityOfHail": 4,
757 | "dayProbabilityOfSferics": 12,
758 | "nightProbabilityOfSferics": 6
759 | }, {
760 | "time": "2024-02-23T00:00Z",
761 | "midday10MWindSpeed": 7.11,
762 | "midnight10MWindSpeed": 4.2,
763 | "midday10MWindDirection": 231,
764 | "midnight10MWindDirection": 232,
765 | "midday10MWindGust": 13.38,
766 | "midnight10MWindGust": 7.16,
767 | "middayVisibility": 23049,
768 | "midnightVisibility": 22325,
769 | "middayRelativeHumidity": 73.25,
770 | "midnightRelativeHumidity": 85.83,
771 | "middayMslp": 98974,
772 | "midnightMslp": 99364,
773 | "maxUvIndex": 2,
774 | "daySignificantWeatherCode": 10,
775 | "nightSignificantWeatherCode": 2,
776 | "dayMaxScreenTemperature": 8.57,
777 | "nightMinScreenTemperature": 3.1,
778 | "dayUpperBoundMaxTemp": 10.67,
779 | "nightUpperBoundMinTemp": 6.84,
780 | "dayLowerBoundMaxTemp": 6.67,
781 | "nightLowerBoundMinTemp": -0.72,
782 | "dayMaxFeelsLikeTemp": 4.42,
783 | "nightMinFeelsLikeTemp": 0.74,
784 | "dayUpperBoundMaxFeelsLikeTemp": 7.38,
785 | "nightUpperBoundMinFeelsLikeTemp": 4.01,
786 | "dayLowerBoundMaxFeelsLikeTemp": 3.89,
787 | "nightLowerBoundMinFeelsLikeTemp": -2.25,
788 | "dayProbabilityOfPrecipitation": 52,
789 | "nightProbabilityOfPrecipitation": 9,
790 | "dayProbabilityOfSnow": 0,
791 | "nightProbabilityOfSnow": 1,
792 | "dayProbabilityOfHeavySnow": 0,
793 | "nightProbabilityOfHeavySnow": 1,
794 | "dayProbabilityOfRain": 52,
795 | "nightProbabilityOfRain": 9,
796 | "dayProbabilityOfHeavyRain": 48,
797 | "nightProbabilityOfHeavyRain": 4,
798 | "dayProbabilityOfHail": 10,
799 | "nightProbabilityOfHail": 1,
800 | "dayProbabilityOfSferics": 11,
801 | "nightProbabilityOfSferics": 1
802 | }]
803 | }
804 | }]
805 | }
806 |
--------------------------------------------------------------------------------
/tests/reference_data/hourly_api_data.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "FeatureCollection",
3 | "parameters": [{
4 | "totalSnowAmount": {
5 | "type": "Parameter",
6 | "description": "Total Snow Amount Over Previous Hour",
7 | "unit": {
8 | "label": "millimetres",
9 | "symbol": {
10 | "value": "http://www.opengis.net/def/uom/UCUM/",
11 | "type": "mm"
12 | }
13 | }
14 | },
15 | "screenTemperature": {
16 | "type": "Parameter",
17 | "description": "Screen Air Temperature",
18 | "unit": {
19 | "label": "degrees Celsius",
20 | "symbol": {
21 | "value": "http://www.opengis.net/def/uom/UCUM/",
22 | "type": "Cel"
23 | }
24 | }
25 | },
26 | "visibility": {
27 | "type": "Parameter",
28 | "description": "Visibility",
29 | "unit": {
30 | "label": "metres",
31 | "symbol": {
32 | "value": "http://www.opengis.net/def/uom/UCUM/",
33 | "type": "m"
34 | }
35 | }
36 | },
37 | "windDirectionFrom10m": {
38 | "type": "Parameter",
39 | "description": "10m Wind From Direction",
40 | "unit": {
41 | "label": "degrees",
42 | "symbol": {
43 | "value": "http://www.opengis.net/def/uom/UCUM/",
44 | "type": "deg"
45 | }
46 | }
47 | },
48 | "precipitationRate": {
49 | "type": "Parameter",
50 | "description": "Precipitation Rate",
51 | "unit": {
52 | "label": "millimetres per hour",
53 | "symbol": {
54 | "value": "http://www.opengis.net/def/uom/UCUM/",
55 | "type": "mm/h"
56 | }
57 | }
58 | },
59 | "maxScreenAirTemp": {
60 | "type": "Parameter",
61 | "description": "Maximum Screen Air Temperature Over Previous Hour",
62 | "unit": {
63 | "label": "degrees Celsius",
64 | "symbol": {
65 | "value": "http://www.opengis.net/def/uom/UCUM/",
66 | "type": "Cel"
67 | }
68 | }
69 | },
70 | "feelsLikeTemperature": {
71 | "type": "Parameter",
72 | "description": "Feels Like Temperature",
73 | "unit": {
74 | "label": "degrees Celsius",
75 | "symbol": {
76 | "value": "http://www.opengis.net/def/uom/UCUM/",
77 | "type": "Cel"
78 | }
79 | }
80 | },
81 | "screenDewPointTemperature": {
82 | "type": "Parameter",
83 | "description": "Screen Dew Point Temperature",
84 | "unit": {
85 | "label": "degrees Celsius",
86 | "symbol": {
87 | "value": "http://www.opengis.net/def/uom/UCUM/",
88 | "type": "Cel"
89 | }
90 | }
91 | },
92 | "screenRelativeHumidity": {
93 | "type": "Parameter",
94 | "description": "Screen Relative Humidity",
95 | "unit": {
96 | "label": "percentage",
97 | "symbol": {
98 | "value": "http://www.opengis.net/def/uom/UCUM/",
99 | "type": "%"
100 | }
101 | }
102 | },
103 | "windSpeed10m": {
104 | "type": "Parameter",
105 | "description": "10m Wind Speed",
106 | "unit": {
107 | "label": "metres per second",
108 | "symbol": {
109 | "value": "http://www.opengis.net/def/uom/UCUM/",
110 | "type": "m/s"
111 | }
112 | }
113 | },
114 | "probOfPrecipitation": {
115 | "type": "Parameter",
116 | "description": "Probability of Precipitation",
117 | "unit": {
118 | "label": "percentage",
119 | "symbol": {
120 | "value": "http://www.opengis.net/def/uom/UCUM/",
121 | "type": "%"
122 | }
123 | }
124 | },
125 | "max10mWindGust": {
126 | "type": "Parameter",
127 | "description": "Maximum 10m Wind Gust Speed Over Previous Hour",
128 | "unit": {
129 | "label": "metres per second",
130 | "symbol": {
131 | "value": "http://www.opengis.net/def/uom/UCUM/",
132 | "type": "m/s"
133 | }
134 | }
135 | },
136 | "significantWeatherCode": {
137 | "type": "Parameter",
138 | "description": "Significant Weather Code",
139 | "unit": {
140 | "label": "dimensionless",
141 | "symbol": {
142 | "value": "https://metoffice.apiconnect.ibmcloud.com/metoffice/production/",
143 | "type": "1"
144 | }
145 | }
146 | },
147 | "minScreenAirTemp": {
148 | "type": "Parameter",
149 | "description": "Minimum Screen Air Temperature Over Previous Hour",
150 | "unit": {
151 | "label": "degrees Celsius",
152 | "symbol": {
153 | "value": "http://www.opengis.net/def/uom/UCUM/",
154 | "type": "Cel"
155 | }
156 | }
157 | },
158 | "totalPrecipAmount": {
159 | "type": "Parameter",
160 | "description": "Total Precipitation Amount Over Previous Hour",
161 | "unit": {
162 | "label": "millimetres",
163 | "symbol": {
164 | "value": "http://www.opengis.net/def/uom/UCUM/",
165 | "type": "mm"
166 | }
167 | }
168 | },
169 | "mslp": {
170 | "type": "Parameter",
171 | "description": "Mean Sea Level Pressure",
172 | "unit": {
173 | "label": "pascals",
174 | "symbol": {
175 | "value": "http://www.opengis.net/def/uom/UCUM/",
176 | "type": "Pa"
177 | }
178 | }
179 | },
180 | "windGustSpeed10m": {
181 | "type": "Parameter",
182 | "description": "10m Wind Gust Speed",
183 | "unit": {
184 | "label": "metres per second",
185 | "symbol": {
186 | "value": "http://www.opengis.net/def/uom/UCUM/",
187 | "type": "m/s"
188 | }
189 | }
190 | },
191 | "uvIndex": {
192 | "type": "Parameter",
193 | "description": "UV Index",
194 | "unit": {
195 | "label": "dimensionless",
196 | "symbol": {
197 | "value": "http://www.opengis.net/def/uom/UCUM/",
198 | "type": "1"
199 | }
200 | }
201 | }
202 | }],
203 | "features": [{
204 | "type": "Feature",
205 | "geometry": {
206 | "type": "Point",
207 | "coordinates": [0.0154, 50.9992, 37.0]
208 | },
209 | "properties": {
210 | "location": {
211 | "name": "Sheffield Park"
212 | },
213 | "requestPointDistance": 1081.5349,
214 | "modelRunDate": "2024-02-15T19:00Z",
215 | "timeSeries": [{
216 | "time": "2024-02-15T19:00Z",
217 | "screenTemperature": 11.0,
218 | "maxScreenAirTemp": 11.55,
219 | "minScreenAirTemp": 10.98,
220 | "screenDewPointTemperature": 8.94,
221 | "feelsLikeTemperature": 10.87,
222 | "windSpeed10m": 1.18,
223 | "windDirectionFrom10m": 180,
224 | "windGustSpeed10m": 6.69,
225 | "max10mWindGust": 8.92,
226 | "visibility": 19174,
227 | "screenRelativeHumidity": 86.99,
228 | "mslp": 100660,
229 | "uvIndex": 0,
230 | "significantWeatherCode": 2,
231 | "precipitationRate": 0.0,
232 | "totalPrecipAmount": 0.0,
233 | "totalSnowAmount": 0,
234 | "probOfPrecipitation": 4
235 | }, {
236 | "time": "2024-02-15T20:00Z",
237 | "screenTemperature": 11.38,
238 | "maxScreenAirTemp": 11.38,
239 | "minScreenAirTemp": 11.0,
240 | "screenDewPointTemperature": 9.55,
241 | "feelsLikeTemperature": 10.96,
242 | "windSpeed10m": 1.68,
243 | "windDirectionFrom10m": 96,
244 | "windGustSpeed10m": 3.55,
245 | "max10mWindGust": 5.64,
246 | "visibility": 17279,
247 | "screenRelativeHumidity": 88.39,
248 | "mslp": 100653,
249 | "uvIndex": 0,
250 | "significantWeatherCode": 2,
251 | "precipitationRate": 0.0,
252 | "totalPrecipAmount": 0.0,
253 | "totalSnowAmount": 0,
254 | "probOfPrecipitation": 1
255 | }, {
256 | "time": "2024-02-15T21:00Z",
257 | "screenTemperature": 11.49,
258 | "maxScreenAirTemp": 11.5,
259 | "minScreenAirTemp": 11.38,
260 | "screenDewPointTemperature": 9.97,
261 | "feelsLikeTemperature": 11.21,
262 | "windSpeed10m": 1.52,
263 | "windDirectionFrom10m": 79,
264 | "windGustSpeed10m": 3.34,
265 | "max10mWindGust": 5.42,
266 | "visibility": 14995,
267 | "screenRelativeHumidity": 90.31,
268 | "mslp": 100714,
269 | "uvIndex": 0,
270 | "significantWeatherCode": 7,
271 | "precipitationRate": 0.0,
272 | "totalPrecipAmount": 0.0,
273 | "totalSnowAmount": 0,
274 | "probOfPrecipitation": 4
275 | }, {
276 | "time": "2024-02-15T22:00Z",
277 | "screenTemperature": 11.18,
278 | "maxScreenAirTemp": 11.49,
279 | "minScreenAirTemp": 11.13,
280 | "screenDewPointTemperature": 9.93,
281 | "feelsLikeTemperature": 10.47,
282 | "windSpeed10m": 2.05,
283 | "windDirectionFrom10m": 112,
284 | "windGustSpeed10m": 4.34,
285 | "max10mWindGust": 5.18,
286 | "visibility": 13525,
287 | "screenRelativeHumidity": 91.97,
288 | "mslp": 100668,
289 | "uvIndex": 0,
290 | "significantWeatherCode": 2,
291 | "precipitationRate": 0.0,
292 | "totalPrecipAmount": 0.0,
293 | "totalSnowAmount": 0,
294 | "probOfPrecipitation": 0
295 | }, {
296 | "time": "2024-02-15T23:00Z",
297 | "screenTemperature": 10.84,
298 | "maxScreenAirTemp": 11.18,
299 | "minScreenAirTemp": 10.78,
300 | "screenDewPointTemperature": 9.7,
301 | "feelsLikeTemperature": 10.15,
302 | "windSpeed10m": 1.96,
303 | "windDirectionFrom10m": 108,
304 | "windGustSpeed10m": 4.74,
305 | "max10mWindGust": 5.33,
306 | "visibility": 12925,
307 | "screenRelativeHumidity": 92.64,
308 | "mslp": 100680,
309 | "uvIndex": 0,
310 | "significantWeatherCode": 2,
311 | "precipitationRate": 0.0,
312 | "totalPrecipAmount": 0.0,
313 | "totalSnowAmount": 0,
314 | "probOfPrecipitation": 1
315 | }, {
316 | "time": "2024-02-16T00:00Z",
317 | "screenTemperature": 10.53,
318 | "maxScreenAirTemp": 10.84,
319 | "minScreenAirTemp": 10.52,
320 | "screenDewPointTemperature": 9.55,
321 | "feelsLikeTemperature": 10.1,
322 | "windSpeed10m": 1.47,
323 | "windDirectionFrom10m": 108,
324 | "windGustSpeed10m": 5.09,
325 | "max10mWindGust": 5.43,
326 | "visibility": 12220,
327 | "screenRelativeHumidity": 93.66,
328 | "mslp": 100650,
329 | "uvIndex": 0,
330 | "significantWeatherCode": 2,
331 | "precipitationRate": 0.0,
332 | "totalPrecipAmount": 0.0,
333 | "totalSnowAmount": 0,
334 | "probOfPrecipitation": 1
335 | }, {
336 | "time": "2024-02-16T01:00Z",
337 | "screenTemperature": 10.72,
338 | "maxScreenAirTemp": 10.78,
339 | "minScreenAirTemp": 10.53,
340 | "screenDewPointTemperature": 9.62,
341 | "feelsLikeTemperature": 10.31,
342 | "windSpeed10m": 1.48,
343 | "windDirectionFrom10m": 135,
344 | "windGustSpeed10m": 5.05,
345 | "max10mWindGust": 5.65,
346 | "visibility": 14094,
347 | "screenRelativeHumidity": 92.91,
348 | "mslp": 100660,
349 | "uvIndex": 0,
350 | "significantWeatherCode": 7,
351 | "precipitationRate": 0.0,
352 | "totalPrecipAmount": 0.0,
353 | "totalSnowAmount": 0,
354 | "probOfPrecipitation": 5
355 | }, {
356 | "time": "2024-02-16T02:00Z",
357 | "screenTemperature": 10.73,
358 | "maxScreenAirTemp": 10.82,
359 | "minScreenAirTemp": 10.65,
360 | "screenDewPointTemperature": 9.75,
361 | "feelsLikeTemperature": 10.51,
362 | "windSpeed10m": 1.17,
363 | "windDirectionFrom10m": 185,
364 | "windGustSpeed10m": 5.33,
365 | "max10mWindGust": 5.73,
366 | "visibility": 13709,
367 | "screenRelativeHumidity": 93.64,
368 | "mslp": 100688,
369 | "uvIndex": 0,
370 | "significantWeatherCode": 2,
371 | "precipitationRate": 0.0,
372 | "totalPrecipAmount": 0.0,
373 | "totalSnowAmount": 0,
374 | "probOfPrecipitation": 2
375 | }, {
376 | "time": "2024-02-16T03:00Z",
377 | "screenTemperature": 10.93,
378 | "maxScreenAirTemp": 10.94,
379 | "minScreenAirTemp": 10.73,
380 | "screenDewPointTemperature": 9.92,
381 | "feelsLikeTemperature": 10.38,
382 | "windSpeed10m": 1.61,
383 | "windDirectionFrom10m": 240,
384 | "windGustSpeed10m": 5.97,
385 | "max10mWindGust": 6.16,
386 | "visibility": 13864,
387 | "screenRelativeHumidity": 93.55,
388 | "mslp": 100744,
389 | "uvIndex": 0,
390 | "significantWeatherCode": 7,
391 | "precipitationRate": 0.0,
392 | "totalPrecipAmount": 0.0,
393 | "totalSnowAmount": 0,
394 | "probOfPrecipitation": 6
395 | }, {
396 | "time": "2024-02-16T04:00Z",
397 | "screenTemperature": 11.03,
398 | "maxScreenAirTemp": 11.05,
399 | "minScreenAirTemp": 10.93,
400 | "screenDewPointTemperature": 10.21,
401 | "feelsLikeTemperature": 9.95,
402 | "windSpeed10m": 2.59,
403 | "windDirectionFrom10m": 279,
404 | "windGustSpeed10m": 7.23,
405 | "max10mWindGust": 7.35,
406 | "visibility": 11033,
407 | "screenRelativeHumidity": 94.71,
408 | "mslp": 100806,
409 | "uvIndex": 0,
410 | "significantWeatherCode": 8,
411 | "precipitationRate": 0.0,
412 | "totalPrecipAmount": 0.0,
413 | "totalSnowAmount": 0,
414 | "probOfPrecipitation": 13
415 | }, {
416 | "time": "2024-02-16T05:00Z",
417 | "screenTemperature": 11.27,
418 | "maxScreenAirTemp": 11.28,
419 | "minScreenAirTemp": 11.03,
420 | "screenDewPointTemperature": 10.54,
421 | "feelsLikeTemperature": 9.74,
422 | "windSpeed10m": 3.47,
423 | "windDirectionFrom10m": 288,
424 | "windGustSpeed10m": 8.23,
425 | "max10mWindGust": 8.45,
426 | "visibility": 14201,
427 | "screenRelativeHumidity": 95.25,
428 | "mslp": 100909,
429 | "uvIndex": 0,
430 | "significantWeatherCode": 12,
431 | "precipitationRate": 0.23,
432 | "totalPrecipAmount": 0.08,
433 | "totalSnowAmount": 0,
434 | "probOfPrecipitation": 43
435 | }, {
436 | "time": "2024-02-16T06:00Z",
437 | "screenTemperature": 10.92,
438 | "maxScreenAirTemp": 11.13,
439 | "minScreenAirTemp": 10.9,
440 | "screenDewPointTemperature": 10.04,
441 | "feelsLikeTemperature": 9.49,
442 | "windSpeed10m": 3.18,
443 | "windDirectionFrom10m": 283,
444 | "windGustSpeed10m": 7.71,
445 | "max10mWindGust": 8.27,
446 | "visibility": 25090,
447 | "screenRelativeHumidity": 94.41,
448 | "mslp": 101042,
449 | "uvIndex": 0,
450 | "significantWeatherCode": 7,
451 | "precipitationRate": 0.0,
452 | "totalPrecipAmount": 0.0,
453 | "totalSnowAmount": 0,
454 | "probOfPrecipitation": 7
455 | }, {
456 | "time": "2024-02-16T07:00Z",
457 | "screenTemperature": 10.62,
458 | "maxScreenAirTemp": 10.92,
459 | "minScreenAirTemp": 10.61,
460 | "screenDewPointTemperature": 9.43,
461 | "feelsLikeTemperature": 8.87,
462 | "windSpeed10m": 3.68,
463 | "windDirectionFrom10m": 279,
464 | "windGustSpeed10m": 8.1,
465 | "max10mWindGust": 8.55,
466 | "visibility": 21863,
467 | "screenRelativeHumidity": 92.4,
468 | "mslp": 101186,
469 | "uvIndex": 0,
470 | "significantWeatherCode": 7,
471 | "precipitationRate": 0.0,
472 | "totalPrecipAmount": 0.0,
473 | "totalSnowAmount": 0,
474 | "probOfPrecipitation": 7
475 | }, {
476 | "time": "2024-02-16T08:00Z",
477 | "screenTemperature": 10.3,
478 | "maxScreenAirTemp": 10.62,
479 | "minScreenAirTemp": 10.27,
480 | "screenDewPointTemperature": 8.77,
481 | "feelsLikeTemperature": 8.27,
482 | "windSpeed10m": 4.15,
483 | "windDirectionFrom10m": 278,
484 | "windGustSpeed10m": 8.77,
485 | "max10mWindGust": 8.86,
486 | "visibility": 17499,
487 | "screenRelativeHumidity": 90.34,
488 | "mslp": 101326,
489 | "uvIndex": 1,
490 | "significantWeatherCode": 3,
491 | "precipitationRate": 0.0,
492 | "totalPrecipAmount": 0.0,
493 | "totalSnowAmount": 0,
494 | "probOfPrecipitation": 1
495 | }, {
496 | "time": "2024-02-16T09:00Z",
497 | "screenTemperature": 10.46,
498 | "maxScreenAirTemp": 10.47,
499 | "minScreenAirTemp": 10.3,
500 | "screenDewPointTemperature": 8.47,
501 | "feelsLikeTemperature": 8.26,
502 | "windSpeed10m": 4.59,
503 | "windDirectionFrom10m": 279,
504 | "windGustSpeed10m": 8.75,
505 | "max10mWindGust": 8.75,
506 | "visibility": 16833,
507 | "screenRelativeHumidity": 87.56,
508 | "mslp": 101456,
509 | "uvIndex": 1,
510 | "significantWeatherCode": 3,
511 | "precipitationRate": 0.0,
512 | "totalPrecipAmount": 0.0,
513 | "totalSnowAmount": 0,
514 | "probOfPrecipitation": 0
515 | }, {
516 | "time": "2024-02-16T10:00Z",
517 | "screenTemperature": 11.07,
518 | "maxScreenAirTemp": 11.09,
519 | "minScreenAirTemp": 10.46,
520 | "screenDewPointTemperature": 8.27,
521 | "feelsLikeTemperature": 8.92,
522 | "windSpeed10m": 4.68,
523 | "windDirectionFrom10m": 276,
524 | "windGustSpeed10m": 8.7,
525 | "max10mWindGust": 8.7,
526 | "visibility": 20678,
527 | "screenRelativeHumidity": 82.98,
528 | "mslp": 101557,
529 | "uvIndex": 1,
530 | "significantWeatherCode": 3,
531 | "precipitationRate": 0.0,
532 | "totalPrecipAmount": 0.0,
533 | "totalSnowAmount": 0,
534 | "probOfPrecipitation": 0
535 | }, {
536 | "time": "2024-02-16T11:00Z",
537 | "screenTemperature": 11.71,
538 | "maxScreenAirTemp": 11.74,
539 | "minScreenAirTemp": 11.07,
540 | "screenDewPointTemperature": 7.9,
541 | "feelsLikeTemperature": 9.42,
542 | "windSpeed10m": 5.16,
543 | "windDirectionFrom10m": 273,
544 | "windGustSpeed10m": 9.42,
545 | "max10mWindGust": 9.42,
546 | "visibility": 30259,
547 | "screenRelativeHumidity": 77.53,
548 | "mslp": 101647,
549 | "uvIndex": 1,
550 | "significantWeatherCode": 1,
551 | "precipitationRate": 0.0,
552 | "totalPrecipAmount": 0.0,
553 | "totalSnowAmount": 0,
554 | "probOfPrecipitation": 0
555 | }, {
556 | "time": "2024-02-16T12:00Z",
557 | "screenTemperature": 12.37,
558 | "maxScreenAirTemp": 12.39,
559 | "minScreenAirTemp": 11.71,
560 | "screenDewPointTemperature": 7.67,
561 | "feelsLikeTemperature": 10.03,
562 | "windSpeed10m": 5.39,
563 | "windDirectionFrom10m": 275,
564 | "windGustSpeed10m": 9.94,
565 | "max10mWindGust": 9.94,
566 | "visibility": 34462,
567 | "screenRelativeHumidity": 73.05,
568 | "mslp": 101700,
569 | "uvIndex": 1,
570 | "significantWeatherCode": 3,
571 | "precipitationRate": 0.0,
572 | "totalPrecipAmount": 0.0,
573 | "totalSnowAmount": 0,
574 | "probOfPrecipitation": 0
575 | }, {
576 | "time": "2024-02-16T13:00Z",
577 | "screenTemperature": 12.91,
578 | "maxScreenAirTemp": 12.94,
579 | "minScreenAirTemp": 12.37,
580 | "screenDewPointTemperature": 7.75,
581 | "feelsLikeTemperature": 10.76,
582 | "windSpeed10m": 4.97,
583 | "windDirectionFrom10m": 271,
584 | "windGustSpeed10m": 9.39,
585 | "max10mWindGust": 9.65,
586 | "visibility": 37584,
587 | "screenRelativeHumidity": 70.9,
588 | "mslp": 101750,
589 | "uvIndex": 1,
590 | "significantWeatherCode": 3,
591 | "precipitationRate": 0.0,
592 | "totalPrecipAmount": 0.0,
593 | "totalSnowAmount": 0,
594 | "probOfPrecipitation": 0
595 | }, {
596 | "time": "2024-02-16T14:00Z",
597 | "screenTemperature": 12.9,
598 | "maxScreenAirTemp": 13.04,
599 | "minScreenAirTemp": 12.69,
600 | "screenDewPointTemperature": 7.06,
601 | "feelsLikeTemperature": 10.54,
602 | "windSpeed10m": 5.39,
603 | "windDirectionFrom10m": 270,
604 | "windGustSpeed10m": 10.16,
605 | "max10mWindGust": 10.16,
606 | "visibility": 38254,
607 | "screenRelativeHumidity": 67.76,
608 | "mslp": 101813,
609 | "uvIndex": 1,
610 | "significantWeatherCode": 3,
611 | "precipitationRate": 0.0,
612 | "totalPrecipAmount": 0.0,
613 | "totalSnowAmount": 0,
614 | "probOfPrecipitation": 1
615 | }, {
616 | "time": "2024-02-16T15:00Z",
617 | "screenTemperature": 12.79,
618 | "maxScreenAirTemp": 12.9,
619 | "minScreenAirTemp": 12.72,
620 | "screenDewPointTemperature": 6.92,
621 | "feelsLikeTemperature": 10.69,
622 | "windSpeed10m": 4.78,
623 | "windDirectionFrom10m": 267,
624 | "windGustSpeed10m": 9.36,
625 | "max10mWindGust": 9.96,
626 | "visibility": 38521,
627 | "screenRelativeHumidity": 67.6,
628 | "mslp": 101887,
629 | "uvIndex": 1,
630 | "significantWeatherCode": 3,
631 | "precipitationRate": 0.0,
632 | "totalPrecipAmount": 0.0,
633 | "totalSnowAmount": 0,
634 | "probOfPrecipitation": 1
635 | }, {
636 | "time": "2024-02-16T16:00Z",
637 | "screenTemperature": 12.06,
638 | "maxScreenAirTemp": 12.79,
639 | "minScreenAirTemp": 12.04,
640 | "screenDewPointTemperature": 6.8,
641 | "feelsLikeTemperature": 10.23,
642 | "windSpeed10m": 4.07,
643 | "windDirectionFrom10m": 266,
644 | "windGustSpeed10m": 8.66,
645 | "max10mWindGust": 9.27,
646 | "visibility": 37284,
647 | "screenRelativeHumidity": 70.37,
648 | "mslp": 101980,
649 | "uvIndex": 1,
650 | "significantWeatherCode": 1,
651 | "precipitationRate": 0.0,
652 | "totalPrecipAmount": 0.0,
653 | "totalSnowAmount": 0,
654 | "probOfPrecipitation": 0
655 | }, {
656 | "time": "2024-02-16T17:00Z",
657 | "screenTemperature": 10.8,
658 | "maxScreenAirTemp": 12.06,
659 | "minScreenAirTemp": 10.77,
660 | "screenDewPointTemperature": 6.94,
661 | "feelsLikeTemperature": 9.36,
662 | "windSpeed10m": 3.16,
663 | "windDirectionFrom10m": 261,
664 | "windGustSpeed10m": 8.0,
665 | "max10mWindGust": 8.43,
666 | "visibility": 33668,
667 | "screenRelativeHumidity": 77.2,
668 | "mslp": 102066,
669 | "uvIndex": 1,
670 | "significantWeatherCode": 1,
671 | "precipitationRate": 0.0,
672 | "totalPrecipAmount": 0.0,
673 | "totalSnowAmount": 0,
674 | "probOfPrecipitation": 0
675 | }, {
676 | "time": "2024-02-16T18:00Z",
677 | "screenTemperature": 9.6,
678 | "maxScreenAirTemp": 10.8,
679 | "minScreenAirTemp": 9.58,
680 | "screenDewPointTemperature": 6.7,
681 | "feelsLikeTemperature": 8.37,
682 | "windSpeed10m": 2.58,
683 | "windDirectionFrom10m": 257,
684 | "windGustSpeed10m": 7.4,
685 | "max10mWindGust": 8.52,
686 | "visibility": 29126,
687 | "screenRelativeHumidity": 82.34,
688 | "mslp": 102166,
689 | "uvIndex": 0,
690 | "significantWeatherCode": 0,
691 | "precipitationRate": 0.0,
692 | "totalPrecipAmount": 0.0,
693 | "totalSnowAmount": 0,
694 | "probOfPrecipitation": 0
695 | }, {
696 | "time": "2024-02-16T19:00Z",
697 | "screenTemperature": 8.94,
698 | "maxScreenAirTemp": 9.6,
699 | "minScreenAirTemp": 8.9,
700 | "screenDewPointTemperature": 6.8,
701 | "feelsLikeTemperature": 7.68,
702 | "windSpeed10m": 2.46,
703 | "windDirectionFrom10m": 255,
704 | "windGustSpeed10m": 7.35,
705 | "max10mWindGust": 8.02,
706 | "visibility": 22767,
707 | "screenRelativeHumidity": 86.81,
708 | "mslp": 102246,
709 | "uvIndex": 0,
710 | "significantWeatherCode": 0,
711 | "precipitationRate": 0.0,
712 | "totalPrecipAmount": 0.0,
713 | "totalSnowAmount": 0,
714 | "probOfPrecipitation": 0
715 | }, {
716 | "time": "2024-02-16T20:00Z",
717 | "screenTemperature": 8.42,
718 | "maxScreenAirTemp": 8.94,
719 | "minScreenAirTemp": 8.41,
720 | "screenDewPointTemperature": 6.3,
721 | "feelsLikeTemperature": 7.06,
722 | "windSpeed10m": 2.45,
723 | "windDirectionFrom10m": 263,
724 | "windGustSpeed10m": 7.47,
725 | "max10mWindGust": 7.99,
726 | "visibility": 21802,
727 | "screenRelativeHumidity": 86.73,
728 | "mslp": 102329,
729 | "uvIndex": 0,
730 | "significantWeatherCode": 0,
731 | "precipitationRate": 0.0,
732 | "totalPrecipAmount": 0.0,
733 | "totalSnowAmount": 0,
734 | "probOfPrecipitation": 0
735 | }, {
736 | "time": "2024-02-16T21:00Z",
737 | "screenTemperature": 7.87,
738 | "maxScreenAirTemp": 8.42,
739 | "minScreenAirTemp": 7.86,
740 | "screenDewPointTemperature": 6.11,
741 | "feelsLikeTemperature": 6.51,
742 | "windSpeed10m": 2.33,
743 | "windDirectionFrom10m": 267,
744 | "windGustSpeed10m": 7.0,
745 | "max10mWindGust": 7.86,
746 | "visibility": 21303,
747 | "screenRelativeHumidity": 88.8,
748 | "mslp": 102406,
749 | "uvIndex": 0,
750 | "significantWeatherCode": 0,
751 | "precipitationRate": 0.0,
752 | "totalPrecipAmount": 0.0,
753 | "totalSnowAmount": 0,
754 | "probOfPrecipitation": 0
755 | }, {
756 | "time": "2024-02-16T22:00Z",
757 | "screenTemperature": 7.48,
758 | "maxScreenAirTemp": 7.87,
759 | "minScreenAirTemp": 7.46,
760 | "screenDewPointTemperature": 5.89,
761 | "feelsLikeTemperature": 6.06,
762 | "windSpeed10m": 2.32,
763 | "windDirectionFrom10m": 274,
764 | "windGustSpeed10m": 6.96,
765 | "max10mWindGust": 7.77,
766 | "visibility": 20523,
767 | "screenRelativeHumidity": 89.86,
768 | "mslp": 102479,
769 | "uvIndex": 0,
770 | "significantWeatherCode": 0,
771 | "precipitationRate": 0.0,
772 | "totalPrecipAmount": 0.0,
773 | "totalSnowAmount": 0,
774 | "probOfPrecipitation": 1
775 | }, {
776 | "time": "2024-02-16T23:00Z",
777 | "screenTemperature": 7.04,
778 | "maxScreenAirTemp": 7.48,
779 | "minScreenAirTemp": 7.01,
780 | "screenDewPointTemperature": 5.66,
781 | "feelsLikeTemperature": 5.68,
782 | "windSpeed10m": 2.18,
783 | "windDirectionFrom10m": 280,
784 | "windGustSpeed10m": 6.69,
785 | "max10mWindGust": 7.53,
786 | "visibility": 20867,
787 | "screenRelativeHumidity": 91.09,
788 | "mslp": 102545,
789 | "uvIndex": 0,
790 | "significantWeatherCode": 0,
791 | "precipitationRate": 0.0,
792 | "totalPrecipAmount": 0.0,
793 | "totalSnowAmount": 0,
794 | "probOfPrecipitation": 1
795 | }, {
796 | "time": "2024-02-17T00:00Z",
797 | "screenTemperature": 6.7,
798 | "maxScreenAirTemp": 7.04,
799 | "minScreenAirTemp": 6.67,
800 | "screenDewPointTemperature": 5.59,
801 | "feelsLikeTemperature": 5.32,
802 | "windSpeed10m": 2.11,
803 | "windDirectionFrom10m": 281,
804 | "windGustSpeed10m": 6.34,
805 | "max10mWindGust": 7.01,
806 | "visibility": 20045,
807 | "screenRelativeHumidity": 92.83,
808 | "mslp": 102614,
809 | "uvIndex": 0,
810 | "significantWeatherCode": 0,
811 | "precipitationRate": 0.0,
812 | "totalPrecipAmount": 0.0,
813 | "totalSnowAmount": 0,
814 | "probOfPrecipitation": 1
815 | }, {
816 | "time": "2024-02-17T01:00Z",
817 | "screenTemperature": 6.26,
818 | "maxScreenAirTemp": 6.7,
819 | "minScreenAirTemp": 6.21,
820 | "screenDewPointTemperature": 5.34,
821 | "feelsLikeTemperature": 4.85,
822 | "windSpeed10m": 2.05,
823 | "windDirectionFrom10m": 283,
824 | "windGustSpeed10m": 6.13,
825 | "max10mWindGust": 6.88,
826 | "visibility": 18378,
827 | "screenRelativeHumidity": 94.11,
828 | "mslp": 102657,
829 | "uvIndex": 0,
830 | "significantWeatherCode": 0,
831 | "precipitationRate": 0.0,
832 | "totalPrecipAmount": 0.0,
833 | "totalSnowAmount": 0,
834 | "probOfPrecipitation": 1
835 | }, {
836 | "time": "2024-02-17T02:00Z",
837 | "screenTemperature": 5.72,
838 | "maxScreenAirTemp": 6.26,
839 | "minScreenAirTemp": 5.66,
840 | "screenDewPointTemperature": 5.02,
841 | "feelsLikeTemperature": 4.45,
842 | "windSpeed10m": 1.73,
843 | "windDirectionFrom10m": 282,
844 | "windGustSpeed10m": 5.73,
845 | "max10mWindGust": 6.69,
846 | "visibility": 14463,
847 | "screenRelativeHumidity": 95.57,
848 | "mslp": 102697,
849 | "uvIndex": 0,
850 | "significantWeatherCode": 0,
851 | "precipitationRate": 0.0,
852 | "totalPrecipAmount": 0.0,
853 | "totalSnowAmount": 0,
854 | "probOfPrecipitation": 2
855 | }, {
856 | "time": "2024-02-17T03:00Z",
857 | "screenTemperature": 5.22,
858 | "maxScreenAirTemp": 5.72,
859 | "minScreenAirTemp": 5.17,
860 | "screenDewPointTemperature": 4.68,
861 | "feelsLikeTemperature": 4.03,
862 | "windSpeed10m": 1.37,
863 | "windDirectionFrom10m": 267,
864 | "windGustSpeed10m": 5.08,
865 | "max10mWindGust": 6.2,
866 | "visibility": 12881,
867 | "screenRelativeHumidity": 96.48,
868 | "mslp": 102729,
869 | "uvIndex": 0,
870 | "significantWeatherCode": 0,
871 | "precipitationRate": 0.0,
872 | "totalPrecipAmount": 0.0,
873 | "totalSnowAmount": 0,
874 | "probOfPrecipitation": 3
875 | }, {
876 | "time": "2024-02-17T04:00Z",
877 | "screenTemperature": 5.44,
878 | "maxScreenAirTemp": 5.46,
879 | "minScreenAirTemp": 5.22,
880 | "screenDewPointTemperature": 4.9,
881 | "feelsLikeTemperature": 4.28,
882 | "windSpeed10m": 1.22,
883 | "windDirectionFrom10m": 234,
884 | "windGustSpeed10m": 4.72,
885 | "max10mWindGust": 5.38,
886 | "visibility": 13816,
887 | "screenRelativeHumidity": 96.53,
888 | "mslp": 102767,
889 | "uvIndex": 0,
890 | "significantWeatherCode": 2,
891 | "precipitationRate": 0.0,
892 | "totalPrecipAmount": 0.0,
893 | "totalSnowAmount": 0,
894 | "probOfPrecipitation": 2
895 | }, {
896 | "time": "2024-02-17T05:00Z",
897 | "screenTemperature": 5.67,
898 | "maxScreenAirTemp": 5.73,
899 | "minScreenAirTemp": 5.44,
900 | "screenDewPointTemperature": 5.3,
901 | "feelsLikeTemperature": 4.55,
902 | "windSpeed10m": 1.28,
903 | "windDirectionFrom10m": 182,
904 | "windGustSpeed10m": 3.99,
905 | "max10mWindGust": 4.95,
906 | "visibility": 4000,
907 | "screenRelativeHumidity": 97.73,
908 | "mslp": 102819,
909 | "uvIndex": 0,
910 | "significantWeatherCode": 7,
911 | "precipitationRate": 0.0,
912 | "totalPrecipAmount": 0.0,
913 | "totalSnowAmount": 0,
914 | "probOfPrecipitation": 5
915 | }, {
916 | "time": "2024-02-17T06:00Z",
917 | "screenTemperature": 6.01,
918 | "maxScreenAirTemp": 6.24,
919 | "minScreenAirTemp": 5.67,
920 | "screenDewPointTemperature": 5.66,
921 | "feelsLikeTemperature": 4.95,
922 | "windSpeed10m": 1.51,
923 | "windDirectionFrom10m": 156,
924 | "windGustSpeed10m": 3.65,
925 | "max10mWindGust": 3.87,
926 | "visibility": 999,
927 | "screenRelativeHumidity": 97.85,
928 | "mslp": 102866,
929 | "uvIndex": 0,
930 | "significantWeatherCode": 6,
931 | "precipitationRate": 0.0,
932 | "totalPrecipAmount": 0.0,
933 | "totalSnowAmount": 0,
934 | "probOfPrecipitation": 17
935 | }, {
936 | "time": "2024-02-17T07:00Z",
937 | "screenTemperature": 6.74,
938 | "maxScreenAirTemp": 6.75,
939 | "minScreenAirTemp": 6.01,
940 | "screenDewPointTemperature": 6.32,
941 | "feelsLikeTemperature": 5.73,
942 | "windSpeed10m": 1.63,
943 | "windDirectionFrom10m": 161,
944 | "windGustSpeed10m": 4.12,
945 | "max10mWindGust": 4.12,
946 | "visibility": 4213,
947 | "screenRelativeHumidity": 97.43,
948 | "mslp": 102918,
949 | "uvIndex": 0,
950 | "significantWeatherCode": 7,
951 | "precipitationRate": 0.0,
952 | "totalPrecipAmount": 0.0,
953 | "totalSnowAmount": 0,
954 | "probOfPrecipitation": 8
955 | }, {
956 | "time": "2024-02-17T08:00Z",
957 | "screenTemperature": 7.5,
958 | "maxScreenAirTemp": 7.54,
959 | "minScreenAirTemp": 6.74,
960 | "screenDewPointTemperature": 7.09,
961 | "feelsLikeTemperature": 6.54,
962 | "windSpeed10m": 1.8,
963 | "windDirectionFrom10m": 171,
964 | "windGustSpeed10m": 4.56,
965 | "max10mWindGust": 4.56,
966 | "visibility": 5736,
967 | "screenRelativeHumidity": 97.44,
968 | "mslp": 102968,
969 | "uvIndex": 1,
970 | "significantWeatherCode": 7,
971 | "precipitationRate": 0.0,
972 | "totalPrecipAmount": 0.0,
973 | "totalSnowAmount": 0,
974 | "probOfPrecipitation": 14
975 | }, {
976 | "time": "2024-02-17T09:00Z",
977 | "screenTemperature": 8.46,
978 | "maxScreenAirTemp": 8.48,
979 | "minScreenAirTemp": 7.5,
980 | "screenDewPointTemperature": 7.89,
981 | "feelsLikeTemperature": 7.54,
982 | "windSpeed10m": 1.9,
983 | "windDirectionFrom10m": 176,
984 | "windGustSpeed10m": 5.23,
985 | "max10mWindGust": 5.23,
986 | "visibility": 5476,
987 | "screenRelativeHumidity": 96.48,
988 | "mslp": 103015,
989 | "uvIndex": 1,
990 | "significantWeatherCode": 7,
991 | "precipitationRate": 0.0,
992 | "totalPrecipAmount": 0.0,
993 | "totalSnowAmount": 0,
994 | "probOfPrecipitation": 15
995 | }, {
996 | "time": "2024-02-17T10:00Z",
997 | "screenTemperature": 9.31,
998 | "maxScreenAirTemp": 9.32,
999 | "minScreenAirTemp": 8.46,
1000 | "screenDewPointTemperature": 8.71,
1001 | "feelsLikeTemperature": 8.16,
1002 | "windSpeed10m": 2.49,
1003 | "windDirectionFrom10m": 194,
1004 | "windGustSpeed10m": 5.87,
1005 | "max10mWindGust": 6.19,
1006 | "visibility": 5671,
1007 | "screenRelativeHumidity": 96.3,
1008 | "mslp": 103039,
1009 | "uvIndex": 1,
1010 | "significantWeatherCode": 8,
1011 | "precipitationRate": 0.0,
1012 | "totalPrecipAmount": 0.0,
1013 | "totalSnowAmount": 0,
1014 | "probOfPrecipitation": 13
1015 | }, {
1016 | "time": "2024-02-17T11:00Z",
1017 | "screenTemperature": 10.23,
1018 | "maxScreenAirTemp": 10.23,
1019 | "minScreenAirTemp": 9.31,
1020 | "screenDewPointTemperature": 9.34,
1021 | "feelsLikeTemperature": 8.78,
1022 | "windSpeed10m": 3.08,
1023 | "windDirectionFrom10m": 202,
1024 | "windGustSpeed10m": 6.31,
1025 | "max10mWindGust": 6.31,
1026 | "visibility": 11241,
1027 | "screenRelativeHumidity": 94.6,
1028 | "mslp": 103069,
1029 | "uvIndex": 1,
1030 | "significantWeatherCode": 8,
1031 | "precipitationRate": 0.0,
1032 | "totalPrecipAmount": 0.0,
1033 | "totalSnowAmount": 0,
1034 | "probOfPrecipitation": 13
1035 | }, {
1036 | "time": "2024-02-17T12:00Z",
1037 | "screenTemperature": 10.79,
1038 | "maxScreenAirTemp": 10.82,
1039 | "minScreenAirTemp": 10.23,
1040 | "screenDewPointTemperature": 9.48,
1041 | "feelsLikeTemperature": 9.18,
1042 | "windSpeed10m": 3.5,
1043 | "windDirectionFrom10m": 203,
1044 | "windGustSpeed10m": 6.77,
1045 | "max10mWindGust": 6.77,
1046 | "visibility": 13088,
1047 | "screenRelativeHumidity": 92.01,
1048 | "mslp": 103068,
1049 | "uvIndex": 1,
1050 | "significantWeatherCode": 8,
1051 | "precipitationRate": 0.0,
1052 | "totalPrecipAmount": 0.0,
1053 | "totalSnowAmount": 0,
1054 | "probOfPrecipitation": 13
1055 | }, {
1056 | "time": "2024-02-17T13:00Z",
1057 | "screenTemperature": 10.84,
1058 | "maxScreenAirTemp": 10.84,
1059 | "minScreenAirTemp": 10.79,
1060 | "screenDewPointTemperature": 9.5,
1061 | "feelsLikeTemperature": 9.17,
1062 | "windSpeed10m": 3.63,
1063 | "windDirectionFrom10m": 202,
1064 | "windGustSpeed10m": 7.09,
1065 | "max10mWindGust": 7.09,
1066 | "visibility": 13756,
1067 | "screenRelativeHumidity": 91.77,
1068 | "mslp": 103050,
1069 | "uvIndex": 1,
1070 | "significantWeatherCode": 8,
1071 | "precipitationRate": 0.0,
1072 | "totalPrecipAmount": 0.0,
1073 | "totalSnowAmount": 0,
1074 | "probOfPrecipitation": 14
1075 | }, {
1076 | "time": "2024-02-17T14:00Z",
1077 | "screenTemperature": 10.63,
1078 | "maxScreenAirTemp": 10.84,
1079 | "minScreenAirTemp": 10.63,
1080 | "screenDewPointTemperature": 9.58,
1081 | "feelsLikeTemperature": 8.92,
1082 | "windSpeed10m": 3.62,
1083 | "windDirectionFrom10m": 201,
1084 | "windGustSpeed10m": 7.07,
1085 | "max10mWindGust": 7.07,
1086 | "visibility": 12109,
1087 | "screenRelativeHumidity": 93.68,
1088 | "mslp": 103021,
1089 | "uvIndex": 1,
1090 | "significantWeatherCode": 8,
1091 | "precipitationRate": 0.0,
1092 | "totalPrecipAmount": 0.0,
1093 | "totalSnowAmount": 0,
1094 | "probOfPrecipitation": 14
1095 | }, {
1096 | "time": "2024-02-17T15:00Z",
1097 | "screenTemperature": 10.62,
1098 | "maxScreenAirTemp": 10.73,
1099 | "minScreenAirTemp": 10.6,
1100 | "screenDewPointTemperature": 9.53,
1101 | "feelsLikeTemperature": 8.92,
1102 | "windSpeed10m": 3.61,
1103 | "windDirectionFrom10m": 200,
1104 | "windGustSpeed10m": 7.22,
1105 | "max10mWindGust": 7.22,
1106 | "visibility": 12463,
1107 | "screenRelativeHumidity": 93.39,
1108 | "mslp": 103003,
1109 | "uvIndex": 1,
1110 | "significantWeatherCode": 8,
1111 | "precipitationRate": 0.0,
1112 | "totalPrecipAmount": 0.0,
1113 | "totalSnowAmount": 0,
1114 | "probOfPrecipitation": 12
1115 | }, {
1116 | "time": "2024-02-17T16:00Z",
1117 | "screenTemperature": 10.57,
1118 | "maxScreenAirTemp": 10.62,
1119 | "minScreenAirTemp": 10.56,
1120 | "screenDewPointTemperature": 9.47,
1121 | "feelsLikeTemperature": 8.88,
1122 | "windSpeed10m": 3.65,
1123 | "windDirectionFrom10m": 197,
1124 | "windGustSpeed10m": 7.38,
1125 | "max10mWindGust": 7.38,
1126 | "visibility": 12932,
1127 | "screenRelativeHumidity": 93.29,
1128 | "mslp": 102986,
1129 | "uvIndex": 1,
1130 | "significantWeatherCode": 8,
1131 | "precipitationRate": 0.0,
1132 | "totalPrecipAmount": 0.0,
1133 | "totalSnowAmount": 0,
1134 | "probOfPrecipitation": 12
1135 | }, {
1136 | "time": "2024-02-17T17:00Z",
1137 | "screenTemperature": 10.45,
1138 | "maxScreenAirTemp": 10.57,
1139 | "minScreenAirTemp": 10.44,
1140 | "screenDewPointTemperature": 9.39,
1141 | "feelsLikeTemperature": 8.75,
1142 | "windSpeed10m": 3.67,
1143 | "windDirectionFrom10m": 191,
1144 | "windGustSpeed10m": 7.54,
1145 | "max10mWindGust": 7.54,
1146 | "visibility": 11295,
1147 | "screenRelativeHumidity": 93.5,
1148 | "mslp": 102968,
1149 | "uvIndex": 1,
1150 | "significantWeatherCode": 8,
1151 | "precipitationRate": 0.0,
1152 | "totalPrecipAmount": 0.0,
1153 | "totalSnowAmount": 0,
1154 | "probOfPrecipitation": 14
1155 | }, {
1156 | "time": "2024-02-17T18:00Z",
1157 | "screenTemperature": 10.25,
1158 | "screenDewPointTemperature": 9.3,
1159 | "feelsLikeTemperature": 8.46,
1160 | "windSpeed10m": 3.77,
1161 | "windDirectionFrom10m": 182,
1162 | "windGustSpeed10m": 7.94,
1163 | "visibility": 10383,
1164 | "screenRelativeHumidity": 94.28,
1165 | "mslp": 102949,
1166 | "uvIndex": 0,
1167 | "significantWeatherCode": 7,
1168 | "precipitationRate": 0.0,
1169 | "probOfPrecipitation": 11
1170 | }, {
1171 | "time": "2024-02-17T19:00Z",
1172 | "screenTemperature": 10.34,
1173 | "screenDewPointTemperature": 9.37,
1174 | "feelsLikeTemperature": 8.34,
1175 | "windSpeed10m": 4.29,
1176 | "windDirectionFrom10m": 187,
1177 | "windGustSpeed10m": 8.68,
1178 | "visibility": 10128,
1179 | "screenRelativeHumidity": 94.17,
1180 | "mslp": 102910,
1181 | "uvIndex": 0,
1182 | "significantWeatherCode": 8,
1183 | "precipitationRate": 0.0,
1184 | "probOfPrecipitation": 16
1185 | }]
1186 | }
1187 | }]
1188 | }
1189 |
--------------------------------------------------------------------------------
/tests/unit/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Perseudonymous/datapoint-python/0261199dc02229fdf0b5b1239431177ecf728c56/tests/unit/__init__.py
--------------------------------------------------------------------------------
/tests/unit/test_forecast.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | import geojson
4 | import pytest
5 |
6 | import tests.reference_data.reference_data_test_forecast as reference_data_test_forecast
7 | from datapoint import Forecast
8 | from datapoint.exceptions import APIException
9 |
10 | # TODO look into pytest-cases. Should reduce the amount of stored data structures
11 |
12 |
13 | @pytest.fixture
14 | def load_hourly_json():
15 | with open("./tests/reference_data/hourly_api_data.json") as f:
16 | my_json = geojson.load(f)
17 | return my_json
18 |
19 |
20 | @pytest.fixture
21 | def load_daily_json():
22 | with open("./tests/reference_data/daily_api_data.json") as f:
23 | my_json = geojson.load(f)
24 | return my_json
25 |
26 |
27 | @pytest.fixture
28 | def load_three_hourly_json():
29 | with open("./tests/reference_data/three_hourly_api_data.json") as f:
30 | my_json = geojson.load(f)
31 | return my_json
32 |
33 |
34 | @pytest.fixture
35 | def daily_forecast(load_daily_json):
36 | return Forecast.Forecast("daily", load_daily_json, convert_weather_code=True)
37 |
38 |
39 | @pytest.fixture
40 | def daily_forecast_raw_weather_code(load_daily_json):
41 | return Forecast.Forecast("daily", load_daily_json, convert_weather_code=False)
42 |
43 |
44 | @pytest.fixture
45 | def twice_daily_forecast(load_daily_json):
46 | return Forecast.Forecast("twice-daily", load_daily_json, convert_weather_code=True)
47 |
48 |
49 | @pytest.fixture
50 | def twice_daily_forecast_raw_weather_code(load_daily_json):
51 | return Forecast.Forecast("twice-daily", load_daily_json, convert_weather_code=False)
52 |
53 |
54 | @pytest.fixture
55 | def hourly_forecast(load_hourly_json):
56 | return Forecast.Forecast("hourly", load_hourly_json, convert_weather_code=True)
57 |
58 |
59 | @pytest.fixture
60 | def hourly_forecast_raw_weather_code(load_hourly_json):
61 | return Forecast.Forecast("hourly", load_hourly_json, convert_weather_code=False)
62 |
63 |
64 | @pytest.fixture
65 | def three_hourly_forecast(load_three_hourly_json):
66 | return Forecast.Forecast(
67 | "three-hourly", load_three_hourly_json, convert_weather_code=True
68 | )
69 |
70 |
71 | @pytest.fixture
72 | def hourly_first_forecast_and_parameters(load_hourly_json):
73 | parameters = load_hourly_json["parameters"][0]
74 | forecast = load_hourly_json["features"][0]["properties"]["timeSeries"][0]
75 | return (forecast, parameters)
76 |
77 |
78 | @pytest.fixture
79 | def three_hourly_first_forecast_and_parameters(load_three_hourly_json):
80 | parameters = load_three_hourly_json["parameters"][0]
81 | forecast = load_three_hourly_json["features"][0]["properties"]["timeSeries"][0]
82 | return (forecast, parameters)
83 |
84 |
85 | @pytest.fixture
86 | def expected_first_hourly_timestep():
87 | return reference_data_test_forecast.EXPECTED_FIRST_HOURLY_TIMESTEP
88 |
89 |
90 | @pytest.fixture
91 | def expected_first_hourly_timestep_raw_weather_code():
92 | return reference_data_test_forecast.EXPECTED_FIRST_HOURLY_TIMESTEP_RAW_WEATHER_CODE
93 |
94 |
95 | @pytest.fixture
96 | def expected_at_datetime_hourly_timestep():
97 | return reference_data_test_forecast.EXPECTED_AT_DATETIME_HOURLY_TIMESTEP
98 |
99 |
100 | @pytest.fixture
101 | def expected_at_datetime_hourly_final_timestep():
102 | return reference_data_test_forecast.EXPECTED_AT_DATETIME_HOURLY_FINAL_TIMESTEP
103 |
104 |
105 | @pytest.fixture
106 | def expected_first_daily_timestep():
107 | return reference_data_test_forecast.EXPECTED_FIRST_DAILY_TIMESTEP
108 |
109 |
110 | @pytest.fixture
111 | def expected_first_daily_timestep_raw_weather_code():
112 | return reference_data_test_forecast.EXPECTED_FIRST_DAILY_TIMESTEP_RAW_WEATHER_CODE
113 |
114 |
115 | @pytest.fixture
116 | def expected_at_datetime_daily_timestep():
117 | return reference_data_test_forecast.EXPECTED_AT_DATETIME_DAILY_TIMESTEP
118 |
119 |
120 | @pytest.fixture
121 | def expected_at_datetime_daily_final_timestep():
122 | return reference_data_test_forecast.EXPECTED_AT_DATETIME_DAILY_FINAL_TIMESTEP
123 |
124 |
125 | @pytest.fixture
126 | def expected_first_twice_daily_timestep():
127 | return reference_data_test_forecast.EXPECTED_FIRST_TWICE_DAILY_TIMESTEP
128 |
129 |
130 | @pytest.fixture
131 | def expected_first_twice_daily_timestep_raw_weather_code():
132 | return (
133 | reference_data_test_forecast.EXPECTED_FIRST_TWICE_DAILY_TIMESTEP_RAW_WEATHER_CODE
134 | )
135 |
136 |
137 | @pytest.fixture
138 | def expected_at_datetime_twice_daily_timestep():
139 | return reference_data_test_forecast.EXPECTED_AT_DATETIME_TWICE_DAILY_TIMESTEP
140 |
141 |
142 | @pytest.fixture
143 | def expected_at_datetime_twice_daily_final_timestep():
144 | return reference_data_test_forecast.EXPECTED_AT_DATETIME_TWICE_DAILY_FINAL_TIMESTEP
145 |
146 |
147 | @pytest.fixture
148 | def expected_first_three_hourly_timestep():
149 | return reference_data_test_forecast.EXPECTED_FIRST_THREE_HOURLY_TIMESTEP
150 |
151 |
152 | @pytest.fixture
153 | def expected_at_datetime_three_hourly_timestep():
154 | return reference_data_test_forecast.EXPECTED_AT_DATETIME_THREE_HOURLY_TIMESTEP
155 |
156 |
157 | @pytest.fixture
158 | def expected_at_datetime_three_hourly_final_timestep():
159 | return reference_data_test_forecast.EXPECTED_AT_DATETIME_THREE_HOURLY_FINAL_TIMESTEP
160 |
161 |
162 | class TestHourlyForecast:
163 | def test_forecast_frequency(self, hourly_forecast):
164 | assert hourly_forecast.frequency == "hourly"
165 |
166 | def test_forecast_location_name(self, hourly_forecast):
167 | assert hourly_forecast.name == "Sheffield Park"
168 |
169 | def test_forecast_location_latitude(self, hourly_forecast):
170 | assert hourly_forecast.forecast_latitude == 50.9992
171 |
172 | def test_forecast_location_longitude(self, hourly_forecast):
173 | assert hourly_forecast.forecast_longitude == 0.0154
174 |
175 | def test_forecast_distance_from_request(self, hourly_forecast):
176 | assert hourly_forecast.distance_from_requested_location == 1081.5349
177 |
178 | def test_forecast_elevation(self, hourly_forecast):
179 | assert hourly_forecast.elevation == 37.0
180 |
181 | def test_forecast_first_timestep(
182 | self, hourly_forecast, expected_first_hourly_timestep
183 | ):
184 | assert hourly_forecast.timesteps[0] == expected_first_hourly_timestep
185 |
186 | def test_build_timestep(
187 | self,
188 | hourly_forecast,
189 | hourly_first_forecast_and_parameters,
190 | expected_first_hourly_timestep,
191 | ):
192 | built_timestep = hourly_forecast._build_timestep(
193 | hourly_first_forecast_and_parameters[0],
194 | hourly_first_forecast_and_parameters[1],
195 | )
196 |
197 | assert built_timestep == expected_first_hourly_timestep
198 |
199 | def test_at_datetime(self, hourly_forecast, expected_at_datetime_hourly_timestep):
200 | ts = hourly_forecast.at_datetime(datetime.datetime(2024, 2, 16, 19, 15))
201 | assert ts == expected_at_datetime_hourly_timestep
202 |
203 | def test_at_datetime_final_timestamp(
204 | self, hourly_forecast, expected_at_datetime_hourly_final_timestep
205 | ):
206 | ts = hourly_forecast.at_datetime(datetime.datetime(2024, 2, 17, 19, 20))
207 | assert ts == expected_at_datetime_hourly_final_timestep
208 |
209 | def test_requested_time_too_early(self, hourly_forecast):
210 | with pytest.raises(APIException):
211 | hourly_forecast.at_datetime(datetime.datetime(2024, 2, 15, 18, 25))
212 |
213 | def test_requested_time_too_late(self, hourly_forecast):
214 | with pytest.raises(APIException):
215 | hourly_forecast.at_datetime(datetime.datetime(2024, 2, 17, 19, 35))
216 |
217 | def test_forecast_first_timestep_raw_weather_code(
218 | self,
219 | hourly_forecast_raw_weather_code,
220 | expected_first_hourly_timestep_raw_weather_code,
221 | ):
222 | assert (
223 | hourly_forecast_raw_weather_code.timesteps[0]
224 | == expected_first_hourly_timestep_raw_weather_code
225 | )
226 |
227 |
228 | class TestDailyForecast:
229 | def test_forecast_frequency(self, daily_forecast):
230 | assert daily_forecast.frequency == "daily"
231 |
232 | def test_forecast_location_name(self, daily_forecast):
233 | assert daily_forecast.name == "Sheffield Park"
234 |
235 | def test_forecast_location_latitude(self, daily_forecast):
236 | assert daily_forecast.forecast_latitude == 50.9992
237 |
238 | def test_forecast_location_longitude(self, daily_forecast):
239 | assert daily_forecast.forecast_longitude == 0.0154
240 |
241 | def test_forecast_distance_from_request(self, daily_forecast):
242 | assert daily_forecast.distance_from_requested_location == 1081.5349
243 |
244 | def test_forecast_elevation(self, daily_forecast):
245 | assert daily_forecast.elevation == 37.0
246 |
247 | def test_forecast_first_timestep(
248 | self, daily_forecast, expected_first_daily_timestep
249 | ):
250 | assert daily_forecast.timesteps[0] == expected_first_daily_timestep
251 |
252 | # Need a new test_build_timestep function here
253 | def test_build_timesteps(
254 | self, daily_forecast, load_daily_json, expected_first_daily_timestep
255 | ):
256 | timestep = daily_forecast._build_timestep(
257 | load_daily_json["features"][0]["properties"]["timeSeries"][0],
258 | load_daily_json["parameters"][0],
259 | )
260 | assert timestep == expected_first_daily_timestep
261 |
262 | def test_at_datetime(self, daily_forecast, expected_at_datetime_daily_timestep):
263 | ts = daily_forecast.at_datetime(datetime.datetime(2024, 2, 16, 19, 15))
264 | assert ts == expected_at_datetime_daily_timestep
265 |
266 | def test_at_datetime_final_timestamp(
267 | self, daily_forecast, expected_at_datetime_daily_final_timestep
268 | ):
269 | ts = daily_forecast.at_datetime(datetime.datetime(2024, 2, 17, 17))
270 | assert ts == expected_at_datetime_daily_final_timestep
271 |
272 | def test_requested_time_too_early(self, daily_forecast):
273 | with pytest.raises(APIException):
274 | daily_forecast.at_datetime(datetime.datetime(2024, 2, 15, 17))
275 |
276 | def test_requested_time_too_late(self, daily_forecast):
277 | with pytest.raises(APIException):
278 | daily_forecast.at_datetime(datetime.datetime(2024, 2, 23, 19))
279 |
280 | def test_forecast_first_timestep_raw_weather_code(
281 | self,
282 | daily_forecast_raw_weather_code,
283 | expected_first_daily_timestep_raw_weather_code,
284 | ):
285 | assert (
286 | daily_forecast_raw_weather_code.timesteps[0]
287 | == expected_first_daily_timestep_raw_weather_code
288 | )
289 |
290 |
291 | class TestTwiceDailyForecast:
292 | def test_forecast_frequency(self, twice_daily_forecast):
293 | assert twice_daily_forecast.frequency == "twice-daily"
294 |
295 | def test_forecast_location_name(self, twice_daily_forecast):
296 | assert twice_daily_forecast.name == "Sheffield Park"
297 |
298 | def test_forecast_location_latitude(self, twice_daily_forecast):
299 | assert twice_daily_forecast.forecast_latitude == 50.9992
300 |
301 | def test_forecast_location_longitude(self, twice_daily_forecast):
302 | assert twice_daily_forecast.forecast_longitude == 0.0154
303 |
304 | def test_forecast_distance_from_request(self, twice_daily_forecast):
305 | assert twice_daily_forecast.distance_from_requested_location == 1081.5349
306 |
307 | def test_forecast_elevation(self, twice_daily_forecast):
308 | assert twice_daily_forecast.elevation == 37.0
309 |
310 | def test_forecast_first_timestep(
311 | self, twice_daily_forecast, expected_first_twice_daily_timestep
312 | ):
313 | assert twice_daily_forecast.timesteps[0] == expected_first_twice_daily_timestep
314 |
315 | # twice-daily forecasts take daily data from DataHub
316 | def test_build_twice_daily_timesteps(
317 | self, twice_daily_forecast, load_daily_json, expected_first_twice_daily_timestep
318 | ):
319 | timesteps = twice_daily_forecast._build_twice_daily_timesteps(
320 | load_daily_json["features"][0]["properties"]["timeSeries"],
321 | load_daily_json["parameters"][0],
322 | )
323 | assert timesteps[0] == expected_first_twice_daily_timestep
324 |
325 | def test_at_datetime(
326 | self, twice_daily_forecast, expected_at_datetime_twice_daily_timestep
327 | ):
328 | ts = twice_daily_forecast.at_datetime(datetime.datetime(2024, 2, 16, 19, 15))
329 | assert ts == expected_at_datetime_twice_daily_timestep
330 |
331 | def test_at_datetime_final_timestep(
332 | self, twice_daily_forecast, expected_at_datetime_twice_daily_final_timestep
333 | ):
334 | ts = twice_daily_forecast.at_datetime(datetime.datetime(2024, 2, 17, 17))
335 | assert ts == expected_at_datetime_twice_daily_final_timestep
336 |
337 | def test_requested_time_too_early(self, twice_daily_forecast):
338 | with pytest.raises(APIException):
339 | twice_daily_forecast.at_datetime(datetime.datetime(2024, 2, 15, 17))
340 |
341 | def test_requested_time_too_late(self, twice_daily_forecast):
342 | with pytest.raises(APIException):
343 | twice_daily_forecast.at_datetime(datetime.datetime(2024, 2, 23, 19))
344 |
345 | def test_forecast_first_timestep_raw_weather_code(
346 | self,
347 | twice_daily_forecast_raw_weather_code,
348 | expected_first_twice_daily_timestep_raw_weather_code,
349 | ):
350 | print(twice_daily_forecast_raw_weather_code.timesteps[0])
351 | assert (
352 | twice_daily_forecast_raw_weather_code.timesteps[0]
353 | == expected_first_twice_daily_timestep_raw_weather_code
354 | )
355 |
356 |
357 | class TestThreeHourlyForecast:
358 | def test_forecast_frequency(self, three_hourly_forecast):
359 | assert three_hourly_forecast.frequency == "three-hourly"
360 |
361 | def test_forecast_location_name(self, three_hourly_forecast):
362 | assert three_hourly_forecast.name == "Sheffield Park"
363 |
364 | def test_forecast_location_latitude(self, three_hourly_forecast):
365 | assert three_hourly_forecast.forecast_latitude == 50.9992
366 |
367 | def test_forecast_location_longitude(self, three_hourly_forecast):
368 | assert three_hourly_forecast.forecast_longitude == 0.0154
369 |
370 | def test_forecast_distance_from_request(self, three_hourly_forecast):
371 | assert three_hourly_forecast.distance_from_requested_location == 1081.5349
372 |
373 | def test_forecast_elevation(self, three_hourly_forecast):
374 | assert three_hourly_forecast.elevation == 37.0
375 |
376 | def test_forecast_first_timestep(
377 | self, three_hourly_forecast, expected_first_three_hourly_timestep
378 | ):
379 | assert (
380 | three_hourly_forecast.timesteps[0] == expected_first_three_hourly_timestep
381 | )
382 |
383 | def test_build_timestep(
384 | self,
385 | three_hourly_forecast,
386 | three_hourly_first_forecast_and_parameters,
387 | expected_first_three_hourly_timestep,
388 | ):
389 | built_timestep = three_hourly_forecast._build_timestep(
390 | three_hourly_first_forecast_and_parameters[0],
391 | three_hourly_first_forecast_and_parameters[1],
392 | )
393 |
394 | assert built_timestep == expected_first_three_hourly_timestep
395 |
396 | def test_at_datetime(
397 | self, three_hourly_forecast, expected_at_datetime_three_hourly_timestep
398 | ):
399 | ts = three_hourly_forecast.at_datetime(datetime.datetime(2024, 2, 22, 19, 15))
400 | assert ts == expected_at_datetime_three_hourly_timestep
401 |
402 | def test_at_datetime_final_timestamp(
403 | self, three_hourly_forecast, expected_at_datetime_three_hourly_final_timestep
404 | ):
405 | ts = three_hourly_forecast.at_datetime(datetime.datetime(2024, 2, 24, 16))
406 | assert ts == expected_at_datetime_three_hourly_final_timestep
407 |
408 | def test_requested_time_too_early(self, three_hourly_forecast):
409 | with pytest.raises(APIException):
410 | three_hourly_forecast.at_datetime(datetime.datetime(2024, 2, 17, 13, 20))
411 |
412 | def test_requested_time_too_late(self, three_hourly_forecast):
413 | with pytest.raises(APIException):
414 | three_hourly_forecast.at_datetime(datetime.datetime(2024, 2, 24, 17))
415 |
--------------------------------------------------------------------------------