├── .dockerignore ├── .github ├── ci-config.yml ├── ci-hpc-config.yml └── workflows │ ├── build-wheel-linux.yml │ ├── build-wheel-macos.yml │ ├── build-wheel-windows.yml │ ├── cd.yml │ ├── check-and-publish.yml │ ├── ci.yml │ ├── label-public-pr.yml │ ├── reusable-ci-hpc.yml │ └── sync.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.rst ├── CONTRIBUTING.rst ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── builder.py ├── ci ├── requirements-dev.txt ├── requirements-docs.in ├── requirements-docs.txt ├── requirements-tests.in └── requirements-tests.txt ├── docs ├── _static │ └── .gitkeep ├── conf.py └── index.rst ├── eccodes-python.code-workspace ├── eccodes ├── .gitignore ├── __init__.py ├── __main__.py ├── _eccodes.cc ├── copying │ └── .gitignore ├── eccodes.py └── highlevel │ ├── __init__.py │ ├── message.py │ └── reader.py ├── gribapi ├── __init__.py ├── bindings.py ├── eccodes.h ├── errors.py ├── grib_api.h └── gribapi.py ├── pytest.ini ├── scripts ├── build-linux.sh ├── build-macos.sh ├── build-windows.sh ├── common.sh ├── copy-dlls.py ├── copy-licences.py ├── requirements.txt ├── select-python-linux.sh ├── select-python-macos.sh ├── test-linux.sh ├── test-macos.sh ├── versions.sh ├── wheel-linux.sh ├── wheel-macos.sh └── wheel-windows.sh ├── setup.cfg ├── setup.py ├── tests ├── requirements.txt ├── sample-data │ ├── era5-levels-members.grib │ └── tiggelam_cnmc_sfc.grib2 ├── test_20_main.py ├── test_20_messages.py ├── test_eccodes.py └── test_highlevel.py └── tox.ini /.dockerignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .docker-tox 3 | .coverage 4 | .eggs 5 | .git 6 | .tox 7 | build 8 | dist 9 | htmlcov 10 | tests/sample-data/* 11 | -------------------------------------------------------------------------------- /.github/ci-config.yml: -------------------------------------------------------------------------------- 1 | dependencies: | 2 | ecmwf/ecbuild 3 | MathisRosenhauer/libaec@master 4 | ecmwf/eccodes 5 | dependency_branch: develop 6 | parallelism_factor: 8 7 | self_build: false 8 | -------------------------------------------------------------------------------- /.github/ci-hpc-config.yml: -------------------------------------------------------------------------------- 1 | build: 2 | python: '3.10' 3 | modules: 4 | - ninja 5 | dependencies: 6 | - ecmwf/ecbuild@develop 7 | - ecmwf/eccodes@develop 8 | env: 9 | - ECCODES_SAMPLES_PATH=$ECCODES_DIR/share/eccodes/samples 10 | - ECCODES_DEFINITION_PATH=$ECCODES_DIR/share/eccodes/definitions 11 | parallel: 64 12 | -------------------------------------------------------------------------------- /.github/workflows/build-wheel-linux.yml: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2024- ECMWF. 2 | # 3 | # This software is licensed under the terms of the Apache Licence Version 2.0 4 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 5 | # In applying this licence, ECMWF does not waive the privileges and immunities 6 | # granted to it by virtue of its status as an intergovernmental organisation 7 | # nor does it submit to any jurisdiction. 8 | 9 | 10 | name: Build Linux 11 | 12 | on: 13 | # Trigger the workflow manually 14 | workflow_dispatch: ~ 15 | 16 | # Allow to be called from another workflow 17 | workflow_call: ~ 18 | 19 | # repository_dispatch: 20 | # types: [eccodes-updated] 21 | 22 | push: 23 | tags-ignore: 24 | - '**' 25 | paths: 26 | - 'scripts/common.sh' 27 | - 'scripts/select-python-linux.sh' 28 | - 'scripts/wheel-linux.sh' 29 | - 'scripts/build-linux.sh' 30 | - 'scripts/test-linux.sh' 31 | - 'scripts/copy-licences.py' 32 | - '.github/workflows/build-wheel-linux.yml' 33 | 34 | # to allow the action to run on the manylinux docker image based on CentOS 7 35 | env: 36 | ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true 37 | 38 | jobs: 39 | 40 | build: 41 | 42 | # if: false # for temporarily disabling for debugging 43 | 44 | runs-on: [self-hosted, Linux, platform-builder-Rocky-8.6] 45 | container: 46 | image: dockcross/manylinux_2_28-x64:20250109-7bf589c 47 | #options: --pull always 48 | 49 | name: Build manylinux_2_28-x64 50 | 51 | steps: 52 | - uses: actions/checkout@v4 53 | 54 | - run: ./scripts/build-linux.sh 55 | 56 | # ################################################################ 57 | - run: ./scripts/wheel-linux.sh 3.9 58 | - uses: actions/upload-artifact@v4 59 | name: Upload wheel 3.9 60 | with: 61 | name: wheel-manylinux2014-3.9 62 | path: wheelhouse/*.whl 63 | 64 | # ################################################################ 65 | - run: ./scripts/wheel-linux.sh 3.10 66 | - uses: actions/upload-artifact@v4 67 | name: Upload wheel 3.10 68 | with: 69 | name: wheel-manylinux2014-3.10 70 | path: wheelhouse/*.whl 71 | 72 | # ################################################################ 73 | - run: ./scripts/wheel-linux.sh 3.11 74 | - uses: actions/upload-artifact@v4 75 | name: Upload wheel 3.11 76 | with: 77 | name: wheel-manylinux2014-3.11 78 | path: wheelhouse/*.whl 79 | 80 | # ################################################################ 81 | - run: ./scripts/wheel-linux.sh 3.12 82 | - uses: actions/upload-artifact@v4 83 | name: Upload wheel 3.12 84 | with: 85 | name: wheel-manylinux2014-3.12 86 | path: wheelhouse/*.whl 87 | 88 | # ################################################################ 89 | - run: ./scripts/wheel-linux.sh 3.13 90 | - uses: actions/upload-artifact@v4 91 | name: Upload wheel 3.13 92 | with: 93 | name: wheel-manylinux2014-3.13 94 | path: wheelhouse/*.whl 95 | 96 | test: 97 | 98 | needs: build 99 | 100 | strategy: 101 | fail-fast: false 102 | matrix: # We don't test 3.6, as it is not supported anymore by github actions 103 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 104 | 105 | runs-on: [self-hosted, Linux, platform-builder-Rocky-8.6] 106 | 107 | name: Test with ${{ matrix.python-version }} 108 | 109 | steps: 110 | 111 | - uses: actions/checkout@v4 112 | 113 | - uses: actions/download-artifact@v4 114 | with: 115 | name: wheel-manylinux2014-${{ matrix.python-version }} 116 | 117 | - run: ./scripts/test-linux.sh ${{ matrix.python-version }} 118 | 119 | 120 | deploy: 121 | 122 | if: ${{ github.ref_type == 'tag' || github.event_name == 'release' }} 123 | 124 | strategy: 125 | fail-fast: false 126 | matrix: 127 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 128 | 129 | needs: [test, build] 130 | 131 | name: Deploy wheel ${{ matrix.python-version }} 132 | 133 | runs-on: [self-hosted, Linux, platform-builder-Rocky-8.6] 134 | 135 | 136 | steps: 137 | 138 | - run: mkdir artifact-${{ matrix.python-version }} 139 | 140 | - uses: actions/checkout@v4 141 | 142 | - uses: actions/download-artifact@v4 143 | with: 144 | name: wheel-manylinux2014-${{ matrix.python-version }} 145 | path: artifact-${{ matrix.python-version }} 146 | 147 | - run: | 148 | source ./scripts/select-python-linux.sh 3.10 149 | pip3 install twine 150 | ls -l artifact-${{ matrix.python-version }}/*.whl 151 | twine upload artifact-${{ matrix.python-version }}/*.whl 152 | env: 153 | TWINE_USERNAME: __token__ 154 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 155 | -------------------------------------------------------------------------------- /.github/workflows/build-wheel-macos.yml: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2024- ECMWF. 2 | # 3 | # This software is licensed under the terms of the Apache Licence Version 2.0 4 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 5 | # In applying this licence, ECMWF does not waive the privileges and immunities 6 | # granted to it by virtue of its status as an intergovernmental organisation 7 | # nor does it submit to any jurisdiction. 8 | 9 | name: Build MacOS ARM 10 | 11 | on: 12 | # Trigger the workflow manually 13 | workflow_dispatch: ~ 14 | 15 | # allow to be called from another workflow 16 | workflow_call: ~ 17 | 18 | # repository_dispatch: 19 | # types: [eccodes-updated] 20 | 21 | push: 22 | tags-ignore: 23 | - '**' 24 | paths: 25 | - 'scripts/common.sh' 26 | - 'scripts/select-python-macos.sh' 27 | - 'scripts/build-macos.sh' 28 | - 'scripts/wheel-macos.sh' 29 | - 'scripts/test-macos.sh' 30 | - 'scripts/copy-licences.py' 31 | - '.github/workflows/build-wheel-macos.yml' 32 | 33 | # We don't use "actions/setup-python@v4" as it installs a universal python 34 | # which creates universal wheels. We want to create wheels for the specific 35 | # architecture we are running on. 36 | 37 | jobs: 38 | 39 | build: 40 | 41 | # if: false # for temporarily disabling for debugging 42 | 43 | strategy: 44 | matrix: 45 | arch_type: [ARM64, X64] 46 | runs-on: [self-hosted, macOS, "${{ matrix.arch_type }}"] 47 | 48 | name: Build 49 | 50 | steps: 51 | 52 | - run: sudo mkdir -p /Users/runner 53 | - run: sudo chown administrator:staff /Users/runner 54 | 55 | - uses: actions/checkout@v2 56 | 57 | - run: ./scripts/build-macos.sh "3.10" 58 | 59 | - run: ./scripts/wheel-macos.sh "3.9" 60 | - run: ls -l wheelhouse 61 | - uses: actions/upload-artifact@v4 62 | name: Upload wheel 3.9 ${{ matrix.arch_type }} 63 | with: 64 | name: wheel-macos-${{ matrix.arch_type }}-3.9 65 | path: wheelhouse/*.whl 66 | - run: rm -fr wheelhouse 67 | 68 | - run: ./scripts/wheel-macos.sh "3.10" 69 | - run: ls -l wheelhouse 70 | - uses: actions/upload-artifact@v4 71 | name: Upload wheel 3.10 ${{ matrix.arch_type }} 72 | with: 73 | name: wheel-macos-${{ matrix.arch_type }}-3.10 74 | path: wheelhouse/*.whl 75 | - run: rm -fr wheelhouse 76 | 77 | - run: ./scripts/wheel-macos.sh "3.11" 78 | - run: ls -l wheelhouse 79 | - uses: actions/upload-artifact@v4 80 | name: Upload wheel 3.11 ${{ matrix.arch_type }} 81 | with: 82 | name: wheel-macos-${{ matrix.arch_type }}-3.11 83 | path: wheelhouse/*.whl 84 | - run: rm -fr wheelhouse 85 | 86 | - run: ./scripts/wheel-macos.sh "3.12" 87 | - run: ls -l wheelhouse 88 | - uses: actions/upload-artifact@v4 89 | name: Upload wheel 3.12 ${{ matrix.arch_type }} 90 | with: 91 | name: wheel-macos-${{ matrix.arch_type }}-3.12 92 | path: wheelhouse/*.whl 93 | - run: rm -fr wheelhouse 94 | 95 | - run: ./scripts/wheel-macos.sh "3.13" 96 | - run: ls -l wheelhouse 97 | - uses: actions/upload-artifact@v4 98 | name: Upload wheel 3.13 ${{ matrix.arch_type }} 99 | with: 100 | name: wheel-macos-${{ matrix.arch_type }}-3.13 101 | path: wheelhouse/*.whl 102 | - run: rm -fr wheelhouse 103 | 104 | test: 105 | needs: build 106 | 107 | strategy: 108 | fail-fast: true 109 | max-parallel: 1 110 | matrix: 111 | arch_type: [ARM64, X64] 112 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 113 | 114 | runs-on: [self-hosted, macOS, "${{ matrix.arch_type }}"] 115 | 116 | name: Test with Python ${{ matrix.python-version }} ${{ matrix.arch_type }} 117 | 118 | steps: 119 | 120 | - uses: actions/checkout@v2 121 | 122 | - uses: actions/download-artifact@v4 123 | with: 124 | name: wheel-macos-${{ matrix.arch_type }}-${{ matrix.python-version }} 125 | 126 | - run: ./scripts/test-macos.sh ${{ matrix.python-version }} 127 | 128 | 129 | deploy: 130 | 131 | if: ${{ github.ref_type == 'tag' || github.event_name == 'release' }} 132 | 133 | needs: [test, build] 134 | 135 | name: Deploy wheel ${{ matrix.python-version }} ${{ matrix.arch_type }} 136 | 137 | strategy: 138 | fail-fast: true 139 | max-parallel: 1 140 | matrix: 141 | arch_type: [ARM64, X64] 142 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 143 | 144 | runs-on: [self-hosted, macOS, "${{ matrix.arch_type }}"] 145 | 146 | steps: 147 | 148 | - run: mkdir artifact-${{ matrix.arch_type }}-${{ matrix.python-version }} 149 | 150 | - uses: actions/checkout@v4 151 | 152 | - uses: actions/download-artifact@v4 153 | with: 154 | name: wheel-macos-${{ matrix.arch_type }}-${{ matrix.python-version }} 155 | path: artifact-${{ matrix.arch_type }}-${{ matrix.python-version }} 156 | 157 | - run: | 158 | source ./scripts/select-python-macos.sh ${{ matrix.python-version }} 159 | VENV_DIR=./dist_venv_${{ matrix.python-version }} 160 | rm -rf ${VENV_DIR} 161 | python3 -m venv ${VENV_DIR} 162 | source ${VENV_DIR}/bin/activate 163 | pip3 install twine 164 | ls -l artifact-${{ matrix.arch_type }}-${{ matrix.python-version }}/*.whl 165 | twine upload artifact-${{ matrix.arch_type }}-${{ matrix.python-version }}/*.whl 166 | env: 167 | TWINE_USERNAME: __token__ 168 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/build-wheel-windows.yml: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2024- ECMWF. 2 | # 3 | # This software is licensed under the terms of the Apache Licence Version 2.0 4 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 5 | # In applying this licence, ECMWF does not waive the privileges and immunities 6 | # granted to it by virtue of its status as an intergovernmental organisation 7 | # nor does it submit to any jurisdiction. 8 | 9 | 10 | name: Build Windows 11 | 12 | on: 13 | # Trigger the workflow manually 14 | workflow_dispatch: ~ 15 | 16 | # Allow to be called from another workflow 17 | workflow_call: ~ 18 | 19 | push: 20 | tags-ignore: 21 | - '**' 22 | paths: 23 | - 'scripts/common.sh' 24 | - 'scripts/wheel-windows.sh' 25 | - 'scripts/build-windows.sh' 26 | - 'scripts/copy-dlls.py' 27 | - 'scripts/copy-licences.py' 28 | - '.github/workflows/build-wheel-windows.yml' 29 | 30 | 31 | jobs: 32 | 33 | build: 34 | 35 | # if: false # for temporarily disabling for debugging 36 | 37 | runs-on: windows-latest 38 | 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | architecture: ["x64"] 43 | 44 | defaults: 45 | run: 46 | shell: bash 47 | 48 | 49 | name: Build on ${{ matrix.architecture }} 50 | env: 51 | WINARCH: ${{ matrix.architecture }} 52 | 53 | steps: 54 | - uses: actions/checkout@v2 55 | 56 | - uses: seanmiddleditch/gha-setup-vsdevenv@master 57 | with: 58 | arch: ${{ matrix.architecture }} 59 | 60 | - name: Set up Python 61 | uses: actions/setup-python@v4 62 | with: 63 | python-version: 3.9 64 | architecture: ${{ matrix.architecture }} 65 | 66 | - run: ./scripts/build-windows.sh 67 | env: 68 | WINARCH: ${{ matrix.architecture }} 69 | 70 | 71 | ################################################################ 72 | 73 | - name: Set up Python 3.9 74 | uses: actions/setup-python@v4 75 | with: 76 | python-version: 3.9 77 | architecture: ${{ matrix.architecture }} 78 | 79 | - run: ./scripts/wheel-windows.sh 3.9 80 | - uses: actions/upload-artifact@v4 81 | name: Upload wheel 3.9 82 | with: 83 | name: wheel-windows-3.9-${{ matrix.architecture }} 84 | path: wheelhouse/*.whl 85 | 86 | ################################################################ 87 | 88 | - name: Set up Python 3.10 89 | uses: actions/setup-python@v4 90 | with: 91 | python-version: "3.10" 92 | architecture: ${{ matrix.architecture }} 93 | 94 | - run: ./scripts/wheel-windows.sh "3.10" 95 | - uses: actions/upload-artifact@v4 96 | name: Upload wheel 3.10 97 | with: 98 | name: wheel-windows-3.10-${{ matrix.architecture }} 99 | path: wheelhouse/*.whl 100 | 101 | ################################################################ 102 | 103 | - name: Set up Python 3.11 104 | uses: actions/setup-python@v4 105 | with: 106 | python-version: "3.11" 107 | architecture: ${{ matrix.architecture }} 108 | 109 | - run: ./scripts/wheel-windows.sh "3.11" 110 | - uses: actions/upload-artifact@v4 111 | name: Upload wheel 3.11 112 | with: 113 | name: wheel-windows-3.11-${{ matrix.architecture }} 114 | path: wheelhouse/*.whl 115 | 116 | ################################################################ 117 | 118 | - name: Set up Python 3.12 119 | uses: actions/setup-python@v4 120 | with: 121 | python-version: "3.12" 122 | architecture: ${{ matrix.architecture }} 123 | 124 | - run: ./scripts/wheel-windows.sh "3.12" 125 | - uses: actions/upload-artifact@v4 126 | name: Upload wheel 3.12 127 | with: 128 | name: wheel-windows-3.12-${{ matrix.architecture }} 129 | path: wheelhouse/*.whl 130 | 131 | ################################################################ 132 | 133 | - name: Set up Python 3.13 134 | uses: actions/setup-python@v4 135 | with: 136 | python-version: "3.13" 137 | architecture: ${{ matrix.architecture }} 138 | 139 | - run: ./scripts/wheel-windows.sh "3.13" 140 | - uses: actions/upload-artifact@v4 141 | name: Upload wheel 3.13 142 | with: 143 | name: wheel-windows-3.13-${{ matrix.architecture }} 144 | path: wheelhouse/*.whl 145 | 146 | ################################################################ 147 | 148 | 149 | test: 150 | needs: build 151 | runs-on: windows-latest 152 | strategy: 153 | fail-fast: true 154 | matrix: 155 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 156 | architecture: ["x64"] 157 | 158 | defaults: 159 | run: 160 | shell: bash 161 | 162 | name: Test with Python ${{ matrix.python-version }} ${{ matrix.architecture }} 163 | 164 | steps: 165 | - uses: actions/checkout@v2 166 | 167 | - name: Set up Python 168 | uses: actions/setup-python@v4 169 | with: 170 | python-version: ${{ matrix.python-version }} 171 | architecture: ${{ matrix.architecture }} 172 | 173 | - uses: actions/download-artifact@v4 174 | with: 175 | name: wheel-windows-${{ matrix.python-version }}-${{ matrix.architecture }} 176 | 177 | - run: pip install *.whl 178 | 179 | - run: pip install -r tests/requirements.txt 180 | 181 | - run: pip freeze 182 | 183 | - run: ECCODES_PYTHON_TRACE_LIB_SEARCH=1 pytest --verbose -s 184 | working-directory: tests 185 | timeout-minutes: 2 186 | 187 | 188 | deploy: 189 | if: ${{ github.ref_type == 'tag' || github.event_name == 'release' }} 190 | 191 | needs: [test, build] 192 | 193 | name: Deploy wheel ${{ matrix.python-version }} ${{ matrix.architecture }} 194 | 195 | runs-on: ubuntu-latest 196 | strategy: 197 | fail-fast: true 198 | matrix: 199 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 200 | architecture: ["x64"] 201 | 202 | steps: 203 | - name: Set up Python 204 | uses: actions/setup-python@v4 205 | with: 206 | python-version: ${{ matrix.python-version }} 207 | 208 | - run: pip install twine 209 | 210 | - uses: actions/download-artifact@v4 211 | with: 212 | name: wheel-windows-${{ matrix.python-version }}-${{ matrix.architecture }} 213 | 214 | - run: twine upload *.whl 215 | env: 216 | TWINE_USERNAME: __token__ 217 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 218 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | # (C) Copyright 2024- ECMWF. 2 | # 3 | # This software is licensed under the terms of the Apache Licence Version 2.0 4 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 5 | # In applying this licence, ECMWF does not waive the privileges and immunities 6 | # granted to it by virtue of its status as an intergovernmental organisation 7 | # nor does it submit to any jurisdiction. 8 | 9 | name: cd 10 | 11 | on: 12 | # Trigger the workflow manually 13 | workflow_dispatch: ~ 14 | 15 | push: 16 | tags: 17 | - '**' 18 | paths: 19 | - '.github/workflows/cd.yml' 20 | jobs: 21 | wheel-linux: 22 | uses: ./.github/workflows/build-wheel-linux.yml 23 | secrets: inherit 24 | wheel-macos: 25 | uses: ./.github/workflows/build-wheel-macos.yml 26 | secrets: inherit 27 | wheel-windows: 28 | uses: ./.github/workflows/build-wheel-windows.yml 29 | secrets: inherit 30 | pypi: 31 | needs: [wheel-linux, wheel-macos, wheel-windows] 32 | uses: ecmwf/reusable-workflows/.github/workflows/cd-pypi.yml@v2 33 | secrets: inherit 34 | 35 | -------------------------------------------------------------------------------- /.github/workflows/check-and-publish.yml: -------------------------------------------------------------------------------- 1 | name: Check and publish 2 | 3 | on: 4 | push: 5 | branches: [master, develop] 6 | 7 | pull_request: 8 | branches: [master, develop] 9 | 10 | release: 11 | types: [created] 12 | 13 | jobs: 14 | quality: 15 | name: Code QA 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - run: pip install black flake8 isort 20 | - run: black --version 21 | - run: isort --check . 22 | - run: black --check . 23 | - run: flake8 . 24 | 25 | checks: 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | platform: [ubuntu-latest, macos-latest, windows-latest] 30 | python-version: ['3.9', '3.10', '3.11', '3.12'] 31 | method: ['conda', 'ecmwflibs'] 32 | exclude: 33 | - platform: macos-latest 34 | python-version: '3.9' 35 | method: 'ecmwflibs' 36 | 37 | name: Python ${{ matrix.python-version }} on ${{ matrix.platform }} (${{ matrix.method }}) 38 | runs-on: ${{ matrix.platform }} 39 | needs: quality 40 | 41 | defaults: 42 | run: 43 | shell: bash 44 | 45 | steps: 46 | - uses: actions/checkout@v3 47 | 48 | - uses: actions/setup-python@v4 49 | with: 50 | python-version: ${{ matrix.python-version }} 51 | 52 | - if: matrix.method == 'conda' 53 | name: Setup conda 54 | uses: s-weigand/setup-conda@v1 55 | with: 56 | update-conda: true 57 | python-version: ${{ matrix.python-version }} 58 | conda-channels: anaconda, conda-forge 59 | 60 | - name: Install tools 61 | run: | 62 | python -m pip install --upgrade pip 63 | pip install pytest pytest-cov isort black flake8 64 | pip install --upgrade setuptools wheel 65 | 66 | - if: matrix.method == 'conda' 67 | run: conda install 'eccodes>=2.27.0' 68 | 69 | - if: matrix.method == 'ecmwflibs' 70 | name: Install ecmwflibs 71 | run: pip install ecmwflibs 72 | 73 | - run: python setup.py develop 74 | - run: pip install -r tests/requirements.txt 75 | - run: pip freeze 76 | - run: env | sort 77 | - run: ECCODES_PYTHON_USE_FINDLIBS=1 python -m eccodes selfcheck 78 | 79 | - run: pytest 80 | if: matrix.method == 'conda' && matrix.platform == 'windows-latest' 81 | env: 82 | ECCODES_DEFINITION_PATH: 'C:/Miniconda/Library/share/eccodes/definitions' 83 | ECCODES_SAMPLES_PATH: 'C:/Miniconda/Library/share/eccodes/samples' 84 | ECCODES_PYTHON_USE_FINDLIBS: '1' 85 | 86 | - run: pytest 87 | if: matrix.method != 'conda' || matrix.platform != 'windows-latest' 88 | env: 89 | ECCODES_PYTHON_USE_FINDLIBS: '1' 90 | # pytest -v --cov=. --cov-report=xml --cov-branch . 91 | 92 | - name: Upload coverage to Codecov 93 | uses: codecov/codecov-action@v1 94 | if: 'false' 95 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | # Trigger the workflow on push to master or develop, except tag creation 5 | push: 6 | branches: 7 | - "master" 8 | - "develop" 9 | tags-ignore: 10 | - "**" 11 | paths-ignore: 12 | - "scripts/**" 13 | - ".github/workflows/*wheel*.yml" 14 | - ".github/workflows/cd.yml" 15 | 16 | # Trigger the workflow on pull request 17 | pull_request: ~ 18 | 19 | # Trigger the workflow manually 20 | workflow_dispatch: ~ 21 | 22 | # Trigger after public PR approved for CI 23 | pull_request_target: 24 | types: [labeled] 25 | 26 | jobs: 27 | # Run CI including downstream packages on self-hosted runners 28 | downstream-ci: 29 | name: downstream-ci 30 | if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }} 31 | uses: ecmwf/downstream-ci/.github/workflows/downstream-ci.yml@main 32 | with: 33 | eccodes-python: ecmwf/eccodes-python@${{ github.event.pull_request.head.sha || github.sha }} 34 | codecov_upload: true 35 | python_qa: true 36 | secrets: inherit 37 | 38 | # Build downstream packages on HPC 39 | downstream-ci-hpc: 40 | name: downstream-ci-hpc 41 | if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }} 42 | uses: ecmwf/downstream-ci/.github/workflows/downstream-ci-hpc.yml@main 43 | with: 44 | eccodes-python: ecmwf/eccodes-python@${{ github.event.pull_request.head.sha || github.sha }} 45 | secrets: inherit 46 | -------------------------------------------------------------------------------- /.github/workflows/label-public-pr.yml: -------------------------------------------------------------------------------- 1 | # Manage labels of pull requests that originate from forks 2 | name: label-public-pr 3 | 4 | on: 5 | pull_request_target: 6 | types: [opened, synchronize] 7 | 8 | jobs: 9 | label: 10 | uses: ecmwf/reusable-workflows/.github/workflows/label-pr.yml@v2 11 | -------------------------------------------------------------------------------- /.github/workflows/reusable-ci-hpc.yml: -------------------------------------------------------------------------------- 1 | name: reusable-ci-hpc 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | eccodes: 7 | required: false 8 | type: string 9 | eccodes-python: 10 | required: false 11 | type: string 12 | 13 | jobs: 14 | ci-hpc: 15 | name: ci-hpc 16 | uses: ecmwf/reusable-workflows/.github/workflows/ci-hpc.yml@v2 17 | with: 18 | name-prefix: eccodes-python- 19 | build-inputs: | 20 | --package: ${{ inputs.eccodes-python || 'ecmwf/eccodes-python@develop' }} 21 | --python: 3.10 22 | --env: | 23 | ECCODES_SAMPLES_PATH=../install/eccodes/share/eccodes/samples 24 | ECCODES_DEFINITION_PATH=../install/eccodes/share/eccodes/definitions 25 | --modules: | 26 | ecbuild 27 | ninja 28 | --dependencies: | 29 | ${{ inputs.eccodes || 'ecmwf/eccodes@develop' }} 30 | --parallel: 64 31 | secrets: inherit 32 | -------------------------------------------------------------------------------- /.github/workflows/sync.yml: -------------------------------------------------------------------------------- 1 | name: sync 2 | 3 | # Controls when the workflow will run 4 | on: 5 | # Trigger the workflow on all pushes 6 | push: 7 | branches: 8 | - "**" 9 | tags: 10 | - "**" 11 | 12 | # Trigger the workflow when a branch or tag is deleted 13 | delete: ~ 14 | 15 | jobs: 16 | # Calls a reusable CI workflow to sync the current with a remote repository. 17 | # It will correctly handle addition of any new and removal of existing Git objects. 18 | sync: 19 | name: sync 20 | uses: ecmwf/reusable-workflows/.github/workflows/sync.yml@v2 21 | secrets: 22 | target_repository: eccodes/eccodes-python 23 | target_username: ClonedDuck 24 | target_token: ${{ secrets.BITBUCKET_PAT }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Static typed files 7 | .pytype 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | dist_venv*/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | gribapi/binary-versions.txt 22 | install/ 23 | lib/ 24 | lib64/ 25 | libs 26 | parts/ 27 | sdist/ 28 | var/ 29 | versions 30 | wheels/ 31 | wheelhouse/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | build-binaries/ 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # Jupyter Notebook 58 | .ipynb_checkpoints 59 | 60 | # pyenv 61 | .python-version 62 | 63 | # Environments 64 | .env 65 | .venv 66 | env/ 67 | venv/ 68 | ENV/ 69 | env.bak/ 70 | venv.bak/ 71 | 72 | # Spyder project settings 73 | .spyderproject 74 | .spyproject 75 | 76 | # Rope project settings 77 | .ropeproject 78 | 79 | # mkdocs documentation 80 | /site 81 | 82 | # mypy 83 | .mypy_cache/ 84 | 85 | # local ignores 86 | .docker-tox/ 87 | *.idx 88 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "restructuredtext.confPath": "" 3 | } -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 2 | Changelog for eccodes-python 3 | ============================ 4 | 5 | 2.42.0 (2025-mm-dd) 6 | -------------------- 7 | 8 | - ECC-2081: Allow memoryview as input to codes_new_from_message 9 | - ECC-2086: GRIB: getting the bitmap using up all the memory 10 | 11 | 12 | 2.41.1 (2025-mm-dd) 13 | -------------------- 14 | 15 | - ECC-2072: high-level 'get' function should return default value if key is not implemented 16 | 17 | 18 | 2.41.0 (2025-04-10) 19 | -------------------- 20 | 21 | - ECC-2034: GRIB encoding: Data quality checks via the API 22 | - GitHub pull request #124: Add basic BUFR support (high-level interface) 23 | 24 | 25 | 2.40.1 (2025-03-17) 26 | -------------------- 27 | 28 | - Update version to be the same as the ecCodes library 29 | 30 | 31 | 2.40.0 (2025-02-12) 32 | -------------------- 33 | 34 | - Update version to be the same as the ecCodes library 35 | 36 | 37 | 2.39.2 (2025-01-27) 38 | -------------------- 39 | 40 | - GitHub pull request #109: Allow setting of array in highlevel.Message.set 41 | - Add support for Python version 3.13 42 | 43 | 44 | 2.39.1 (2024-12-10) 45 | -------------------- 46 | 47 | - Build wheel with thread-safety enabled 48 | 49 | 2.39.0 (2024-11-25) 50 | -------------------- 51 | 52 | - ECC-1972: Support Windows with binary wheel 53 | - Update to artifact actions v4 54 | 55 | 2.38.3 (2024-10-17) 56 | -------------------- 57 | 58 | - Update version to be the same as the ecCodes library 59 | 60 | 2.38.1 (2024-09-26) 61 | -------------------- 62 | 63 | - ECC-1923: ecCodes binary wheel can affect floating-point computations in Python 64 | 65 | 66 | 2.38.0 (2024-09-25) 67 | -------------------- 68 | 69 | - ECC-1790: Add codes_get_offset 70 | - ECC-1899: API function to allow setting debug level 71 | - Function to query library features 72 | 73 | 2.37.0 (2024-09-09) 74 | ------------------- 75 | 76 | - bundle ecCodes binary library with the PyPi distribution, for Linux and MacOS 77 | 78 | 79 | 1.7.1 (2024-06-19) 80 | -------------------- 81 | 82 | - `np.Infinity` was removed in the NumPy 2.0 release 83 | 84 | 1.7.0 (2024-02-26) 85 | -------------------- 86 | 87 | - ECC-1761: Add function to extract message offsets and sizes 88 | - ECC-1742: Add function to clone only the meta-data of a message 89 | 90 | 1.6.1 (2023-10-02) 91 | -------------------- 92 | 93 | - ECC-1693: Update minimum recommended version 94 | - Fix flake8 warning E721 95 | 96 | 1.6.0 (2023-07-11) 97 | -------------------- 98 | 99 | - ECC-1630: Get API version as an integer 100 | - ECC-1622: Drop Python version 3.7 101 | - ECC-1601: GRIB: Support data values array decoded in single-precision 102 | - ECC-1611: Add function to determine if a BUFR key is a coordinate descriptor 103 | 104 | 1.5.2 (2023-04-04) 105 | -------------------- 106 | 107 | - Add support for Python versions 3.10 and 3.11 108 | - ECC-1555: 2D numpy array incorrectly handled 109 | - ECC-1539: Use the 'warnings' library for selfcheck 110 | - ECC-1538: Add support for CODES_TYPE_BYTES 111 | - ECC-1524: Check values in High-level Message.set function should retrieve based on value type 112 | - ECC-1527: Handle floats in high-level Message.set function check values 113 | 114 | 115 | 1.5.1 (2023-01-25) 116 | -------------------- 117 | 118 | - ECC-1446: Data file era5-levels-members.grib not included in released tar file 119 | - ECC-1460: Cannot import eccodes on M1 MacBook Pro 120 | - ECC-1505: High-level Message.set function should allow dictionary and check result 121 | 122 | 1.5.0 (2022-08-25) 123 | -------------------- 124 | 125 | - ECC-1404: Add the grib_get_gaussian_latitudes() function 126 | - ECC-1405: Add new function: codes_any_new_from_samples 127 | - ECC-1415: Implement a higher-level Python interface (still experimental) 128 | - ECC-1429: Remove the file 'eccodes/messages.py' 129 | - GitHub pull request #62: add pypi badge 130 | 131 | 1.4.2 (2022-05-20) 132 | -------------------- 133 | 134 | - ECC-1389: Drop Python version 3.5 and 3.6 135 | - ECC-1390: NameError: name 'GribInternalError' is not defined 136 | - Add test for GRIB bitmap 137 | 138 | 139 | 1.4.1 (2022-03-03) 140 | -------------------- 141 | 142 | - ECC-1351: Support numpy.int64 in codes_set() and codes_set_long() 143 | - ECC-1317: Data file tiggelam_cnmc_sfc.grib2 not included in released tar file 144 | 145 | 146 | 1.4.0 (2021-12-03) 147 | -------------------- 148 | 149 | - ECC-1234: Remove the experimental high-level interface 150 | - ECC-1282: Add codes_dump() 151 | 152 | 153 | 1.3.4 (2021-08-27) 154 | -------------------- 155 | 156 | - Update documentation 157 | 158 | 159 | 1.3.3 (2021-06-21) 160 | -------------------- 161 | 162 | - ECC-1246: UnicodeDecodeError when parsing BUFR file 163 | 164 | 165 | 1.3.2 (2021-04-16) 166 | -------------------- 167 | 168 | - Restore the experimental high-level interface 169 | 170 | 171 | 1.3.1 (2021-04-16) 172 | -------------------- 173 | 174 | - Fix the recommended version 175 | 176 | 177 | 1.3.0 (2021-04-09) 178 | -------------------- 179 | 180 | - ECC-1231: Remove the experimental high-level interface 181 | - Added the "findlibs" module 182 | - Fix tests/test_high_level_api.py when MEMFS enabled 183 | - ECC-1226: Python3 bindings: Typo causes AttributeError when calling codes_index_get_double 184 | 185 | 186 | 1.2.0 (2021-03-23) 187 | -------------------- 188 | 189 | - Added test for multi-field GRIBs 190 | - Fix deprecation warning: `np.float` is a deprecated alias for the builtin `float` 191 | - Experimental feature: grib_nearest_find 192 | 193 | 194 | 1.1.0 (2021-01-20) 195 | -------------------- 196 | 197 | - ECC-1171: Performance: Python bindings: remove assert statements 198 | - ECC-1161: Python3 bindings: Do not raise exception on first failed attempt 199 | - ECC-1176: Python3 bindings: float32 recognised as int instead of float 200 | - GitHub pull request #41: Remove the apparent support for Python 2 201 | - GitHub pull request #44: Fix CFFI crash on windows 202 | - GitHub pull request #42: Add unit testing with GitHub actions (linux, macos and windows) 203 | 204 | 205 | 1.0.0 (2020-10-14) 206 | -------------------- 207 | 208 | - ECC-1143: CMake: Migration to ecbuild v3.4 209 | - ECC-1133: C API: Propagate const char* for codes_index_new_from_file and codes_index_select_string 210 | 211 | 212 | 0.9.9 (2020-08-04) 213 | ------------------- 214 | 215 | - Support for ecmwflibs. An additional way to find ECMWF libraries (if available) 216 | - ECC-1140: Segfault from invalid pointer reference in grib_set_double_array() 217 | 218 | 219 | 0.9.8 (2020-06-26) 220 | ------------------- 221 | 222 | - ECC-1110: Removed obsolete function codes_close_file() 223 | - Provide missing argument to exceptions 224 | - Fix codes_set_definitions_path() typo 225 | - Fix grib_get_double_element(). Missing last argument 226 | - Add more tests to increase coverage 227 | - GitHub pull request #15: Add .__next__() method to eccodes.CodesFile class 228 | - ECC-1113: Python3 bindings under Windows: codes_get_long_array returns incorrect values 229 | - ECC-1108: Python3 bindings under Windows: use of handle causes crash 230 | - ECC-1121: Segfault when closing GribFile if messages are closed manually 231 | 232 | 233 | 0.9.6 (2020-03-10) 234 | ------------------- 235 | 236 | - Update Copyright notices 237 | - Function-argument type checks: Improve error message 238 | - Fix C function calls for codes_gribex_mode_on/codes_gribex_mode_off 239 | 240 | 241 | 0.9.5 (2020-01-15) 242 | ------------------- 243 | 244 | - ECC-1029: Function-argument type-checking should be disabled by default. 245 | To enable these checks, export ECCODES_PYTHON_ENABLE_TYPE_CHECKS=1 246 | - ECC-1032: Added codes_samples_path() and codes_definition_path() 247 | - ECC-1042: Python3 interface writes integer arrays incorrectly 248 | - ECC-794: Python3 interface: Expose the grib_get_data function 249 | 250 | 251 | 0.9.4 (2019-11-27) 252 | ------------------ 253 | 254 | - Added new function: codes_get_version_info 255 | - ECC-753: Expose the codes_grib_nearest_find_multiple function in Python 256 | - ECC-1007: Python3 interface for eccodes cannot write large arrays 257 | 258 | 259 | 0.9.3 (2019-10-04) 260 | ------------------ 261 | 262 | - New exception added: FunctionalityNotEnabledError 263 | - BUFR decoding: support for multi-element constant arrays (ECC-428) 264 | 265 | 266 | 0.9.2 (2019-07-09) 267 | ------------------ 268 | 269 | - All ecCodes tests now pass 270 | - Simplify the xx_new_from_file calls 271 | - Fix for grib_set_string_array 272 | - Use ECCODES_DIR to locate the library 273 | - Remove the new-style high-level interface. It is still available in 274 | `cfgrib `_. 275 | 276 | 0.9.1 (2019-06-06) 277 | ------------------ 278 | 279 | - ``codes_get_long_array`` and ``codes_get_double_array`` now return a ``np.ndarray``. 280 | See: `#3 `_. 281 | 282 | 283 | 0.9.0 (2019-05-07) 284 | ------------------ 285 | 286 | - Declare the project as **Beta**. 287 | 288 | 289 | 0.8.0 (2019-04-08) 290 | ------------------ 291 | 292 | - First public release. 293 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | 2 | .. highlight: console 3 | 4 | ============ 5 | Contributing 6 | ============ 7 | 8 | Contributions are welcome, and they are greatly appreciated! Every 9 | little bit helps, and credit will always be given. 10 | 11 | Please note, that we have hooked a CLA assistant to this GitHub Repo. Please accept the contributors license agreement to allow us to keep a legal track of contributions and keep this package open source for the future. 12 | 13 | You can contribute in many ways: 14 | 15 | Types of Contributions 16 | ---------------------- 17 | 18 | Report Bugs 19 | ~~~~~~~~~~~ 20 | 21 | Report bugs at https://github.com/ecmwf/eccodes-python/issues 22 | 23 | If you are reporting a bug, please include: 24 | 25 | * Your operating system name and version. 26 | * Installation method and version of all dependencies. 27 | * Any details about your local setup that might be helpful in troubleshooting. 28 | * Detailed steps to reproduce the bug, including a sample file. 29 | 30 | Fix Bugs 31 | ~~~~~~~~ 32 | 33 | Look through the GitHub issues for bugs. Anything tagged with "bug" 34 | and "help wanted" is open to whoever wants to implement a fix for it. 35 | 36 | Implement Features 37 | ~~~~~~~~~~~~~~~~~~ 38 | 39 | Look through the GitHub issues for features. Anything tagged with "enhancement" 40 | and "help wanted" is open to whoever wants to implement it. 41 | 42 | Get Started! 43 | ------------ 44 | 45 | Ready to contribute? Here's how to set up `eccodes-python` for local development. Please note this documentation assumes 46 | you already have `virtualenv` and `Git` installed and ready to go. 47 | 48 | 1. Fork the `eccodes-python` repo on GitHub. 49 | 2. Clone your fork locally:: 50 | 51 | $ cd path_for_the_repo 52 | $ git clone https://github.com/YOUR_NAME/eccodes-python.git 53 | $ cd eccodes-python 54 | 55 | 3. Assuming you have virtualenv installed (If you have Python3.5 this should already be there), you can create a new environment for your local development by typing:: 56 | 57 | $ virtualenv ../eccodes-python-env 58 | $ source ../eccodes-python-env/bin/activate 59 | 60 | This should change the shell to look something like 61 | (eccodes-python-env) $ 62 | 63 | 4. Install system dependencies as described in the README.rst file then install a known-good set of python dependencies and the your local copy with:: 64 | 65 | $ pip install -r ci/requirements-tests.txt 66 | $ pip install -e . 67 | 68 | 5. Create a branch for local development:: 69 | 70 | $ git checkout -b name-of-your-bugfix-or-feature 71 | 72 | Now you can make your changes locally. 73 | 74 | 6. The next step would be to run the test cases. `eccodes-python` uses py.test, you can run PyTest. Before you run pytest you should ensure all dependencies are installed:: 75 | 76 | $ pip install -r ci/requirements-dev.txt 77 | $ pytest -v --flakes 78 | 79 | 7. Before raising a pull request you should also run tox. This will run the tests across different versions of Python:: 80 | 81 | $ tox 82 | 83 | 8. If your contribution is a bug fix or new feature, you should add a test to the existing test suite. 84 | 85 | 9. Format your Python code with the Black auto-formatter, to ensure the code is uses the library's style. We use the default Black configuration (88 lines per character and `"` instead of `'` for string encapsulation):: 86 | 87 | $ black . 88 | 89 | 10. Commit your changes and push your branch to GitHub:: 90 | 91 | $ git add . 92 | $ git commit -m "Your detailed description of your changes." 93 | $ git push origin name-of-your-bugfix-or-feature 94 | 95 | 11. Submit a pull request through the GitHub website. 96 | 97 | Pull Request Guidelines 98 | ----------------------- 99 | 100 | Before you submit a pull request, check that it meets these guidelines: 101 | 102 | 1. The pull request should include tests. 103 | 104 | 2. If the pull request adds functionality, the docs should be updated. Put 105 | your new functionality into a function with a docstring, and add the 106 | feature to the list in README.rst. 107 | 108 | 3. The pull request should work for Python 3.9, 3.10, 3.11, 3.12, 3.13 and for PyPy2 and PyPy3. 109 | Check the tox results and make sure that the tests pass for all supported Python versions. 110 | 111 | 112 | Testing CDS data 113 | ---------------- 114 | 115 | You can test the CF-GRIB driver on a set of products downloaded from the Climate Data Store 116 | of the `Copernicus Climate Change Service `_. 117 | If you are not register to the CDS portal register at: 118 | 119 | https://cds.climate.copernicus.eu/user/register 120 | 121 | In order to automatically download and test the GRIB files install and configure the `cdsapi` package:: 122 | 123 | $ pip install cdsapi 124 | $ pip install netcdf4 125 | 126 | The log into the CDS portal and setup the CDS API key as described in: 127 | 128 | https://cds.climate.copernicus.eu/api-how-to 129 | 130 | Then you can run:: 131 | 132 | $ pytest -vv tests/cds_test_*.py 133 | 134 | 135 | .. eccodes-python: https://github.com/ecmwf/eccodes-python 136 | .. virtualenv: https://virtualenv.pypa.io/en/stable/installation 137 | .. git: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git 138 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Run tests in a more reproducible and isolated environment. 2 | # 3 | # Build the docker image once with: 4 | # docker build -t eccodes . 5 | # Run the container with: 6 | # docker run --rm -it -v `pwd`:/src eccodes-python 7 | # 8 | FROM bopen/ubuntu-pyenv:latest 9 | 10 | ARG DEBIAN_FRONTEND="noninteractive" 11 | 12 | RUN apt-get -y update && apt-get install -y --no-install-recommends \ 13 | libeccodes0 \ 14 | && rm -rf /var/lib/apt/lists/* 15 | 16 | COPY . /src/ 17 | 18 | RUN cd /src \ 19 | && make local-install-test-req \ 20 | && make local-develop \ 21 | && make local-install-dev-req \ 22 | && make distclean 23 | 24 | WORKDIR /src 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017- ECMWF 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include .dockerignore 2 | include *.rst 3 | include *.yml 4 | include Dockerfile 5 | include LICENSE 6 | include Makefile 7 | include tox.ini 8 | include *.py 9 | recursive-include ci *.in 10 | recursive-include ci *.txt 11 | recursive-include ci *.yml 12 | recursive-include ci *.ps1 13 | recursive-include docs *.gitkeep 14 | recursive-include docs *.py 15 | recursive-include docs *.rst 16 | recursive-include gribapi *.h 17 | recursive-include tests *.grib2 18 | recursive-include tests *.grib 19 | recursive-include tests *.ipynb 20 | recursive-include tests *.py 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | PACKAGE := eccodes-python 3 | IMAGE := $(PACKAGE)-image 4 | MODULE := eccodes 5 | PYTHONS := python3.9 pypy3 6 | PYTHON := python 7 | 8 | PYTESTFLAGS_TEST := -v --flakes --doctest-glob '*.rst' --cov=$(MODULE) --cov-report=html --cache-clear 9 | PYTESTFLAGS_QC := --pep8 --mccabe $(PYTESTFLAGS_TEST) 10 | 11 | export WHEELHOUSE := ~/.wheelhouse 12 | export PIP_FIND_LINKS := $(WHEELHOUSE) 13 | export PIP_WHEEL_DIR := $(WHEELHOUSE) 14 | export PIP_INDEX_URL 15 | 16 | DOCKERBUILDFLAGS := --build-arg PIP_INDEX_URL=$(PIP_INDEX_URL) 17 | DOCKERFLAGS := -e WHEELHOUSE=$(WHEELHOUSE) \ 18 | -e PIP_FIND_LINKS=$(PIP_FIND_LINKS) \ 19 | -e PIP_WHEEL_DIR=$(PIP_WHEEL_DIR) \ 20 | -e PIP_INDEX_URL=$(PIP_INDEX_URL) 21 | PIP := $(PYTHON) -m pip 22 | MKDIR = mkdir -p 23 | 24 | ifeq ($(shell [ -d $(WHEELHOUSE) ] && echo true),true) 25 | DOCKERFLAGS += -v $(WHEELHOUSE):/root/.wheelhouse 26 | endif 27 | 28 | RUNTIME := $(shell [ -f /proc/1/cgroup ] && cat /proc/1/cgroup | grep -q docker && echo docker) 29 | ifneq ($(RUNTIME),docker) 30 | override TOXFLAGS += --workdir=.docker-tox 31 | RUN = docker run --rm -it -v$$(pwd):/src -w/src $(DOCKERFLAGS) $(IMAGE) 32 | endif 33 | 34 | 35 | default: 36 | @echo No default 37 | 38 | # local targets 39 | 40 | $(PIP_FIND_LINKS): 41 | $(MKDIR) $@ 42 | 43 | local-wheelhouse-one: 44 | $(PIP) install wheel 45 | $(PIP) wheel -r ci/requirements-tests.txt 46 | $(PIP) wheel -r ci/requirements-docs.txt 47 | 48 | local-wheelhouse: 49 | for PYTHON in $(PYTHONS); do $(MAKE) local-wheelhouse-one PYTHON=$$PYTHON; done 50 | $(PIP) wheel -r ci/requirements-dev.txt 51 | 52 | local-install-dev-req: 53 | $(PIP) install -r ci/requirements-dev.txt 54 | 55 | local-install-test-req: $(PIP_FIND_LINKS) 56 | $(PIP) install -r ci/requirements-tests.txt 57 | $(PIP) install -r ci/requirements-docs.txt 58 | 59 | local-develop: 60 | $(PIP) install -e . 61 | 62 | local-wheel: 63 | $(PIP) wheel -e . 64 | 65 | testclean: 66 | $(RM) -r */__pycache__ .coverage .cache tests/.ipynb_checkpoints *.idx tests/sample-data/*.idx out*.grib 67 | 68 | clean: testclean 69 | $(RM) -r */*.pyc htmlcov dist build .eggs 70 | 71 | distclean: clean 72 | $(RM) -r .tox .docker-tox *.egg-info 73 | 74 | cacheclean: 75 | $(RM) -r $(WHEELHOUSE)/* ~/.cache/* 76 | 77 | # container targets 78 | 79 | shell: 80 | $(RUN) 81 | 82 | notebook: DOCKERFLAGS += -p 8888:8888 83 | notebook: 84 | $(RUN) jupyter notebook --ip=0.0.0.0 --allow-root 85 | 86 | wheelhouse: 87 | $(RUN) make local-wheelhouse 88 | 89 | update-req: 90 | $(RUN) pip-compile -o ci/requirements-tests.txt -U setup.py ci/requirements-tests.in 91 | $(RUN) pip-compile -o ci/requirements-docs.txt -U setup.py ci/requirements-docs.in 92 | 93 | test: testclean 94 | $(RUN) $(PYTHON) setup.py test --addopts "$(PYTESTFLAGS_TEST)" 95 | 96 | qc: testclean 97 | $(RUN) $(PYTHON) setup.py test --addopts "$(PYTESTFLAGS_QC)" 98 | 99 | doc: 100 | $(RUN) $(PYTHON) setup.py build_sphinx 101 | 102 | tox: testclean 103 | $(RUN) tox $(TOXFLAGS) 104 | 105 | detox: testclean 106 | $(RUN) detox $(TOXFLAGS) 107 | 108 | # image build 109 | 110 | image: 111 | docker build -t $(IMAGE) $(DOCKERBUILDFLAGS) . 112 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/pypi/v/eccodes.svg 2 | :target: https://pypi.python.org/pypi/eccodes/ 3 | 4 | Python 3 interface to decode and encode GRIB and BUFR files via the 5 | `ECMWF ecCodes library `_. 6 | 7 | Features: 8 | 9 | - reads and writes GRIB 1 and 2 files, 10 | - reads and writes BUFR 3 and 4 files, 11 | - supports all modern versions of Python and PyPy3, 12 | - works on most *Linux* distributions and *MacOS*, the *ecCodes* C-library 13 | is the only system dependency, 14 | - PyPI package can be installed without compiling, 15 | at the cost of being twice as slow as the original *ecCodes* module, 16 | - an optional compile step makes the code as fast as the original module 17 | but it needs the recommended (the most up-to-date) version of *ecCodes*. 18 | 19 | Limitations: 20 | 21 | - Microsoft Windows support is untested. 22 | 23 | 24 | Installation 25 | ============ 26 | 27 | **From version 2.37.0, the ecCodes Python bindings on PyPi additionally provide the ecCodes binary library, and will 28 | follow the version numbering of the ecCodes binary library. See below for details.** 29 | 30 | Installation from PyPI 31 | ---------------------- 32 | 33 | The package can be installed from PyPI with:: 34 | 35 | $ pip install eccodes 36 | 37 | This installation will, by default, include the ecCodes binary library (as of version 2.37.0), meaning that no 38 | external ecCodes binary library is required. If you have an external ecCodes binary library that you wish to use, 39 | set the following environment variable before you import eccodes:: 40 | 41 | $ export ECCODES_PYTHON_USE_FINDLIBS=1 42 | 43 | If this is set, the ecCodes' Python bindings will use the `findlibs `_ package 44 | to locate the binary library (findlibs was the only mechanism used before version 2.37.0). 45 | 46 | You may also install a version of ecCodes' Python interface that does not include a binary library at all, 47 | in which case the findlibs mechanism will be used as before:: 48 | 49 | $ pip install eccodes --no-binary eccodes 50 | 51 | See also 'Debugging the library search', below. 52 | 53 | 54 | Installation from conda 55 | ----------------------- 56 | 57 | ecCodes' Python bindings can be installed from the `conda-forge `_ channel with:: 58 | 59 | $ conda install -c conda-forge python-eccodes 60 | 61 | This will install the Python bindings (`python-eccodes`) and also the ecCodes binary library (`eccodes`) on which they depend. 62 | 63 | 64 | System dependencies 65 | ------------------- 66 | 67 | The Python module depends on the ECMWF *ecCodes* binary library. From version 2.37.0, this library is supplied with 68 | the Python module on both PyPi and conda, as described above. If you wish to install and use a separate binary library 69 | (see above), it must be installed on the system and accessible as a shared library. 70 | 71 | On a MacOS with HomeBrew use:: 72 | 73 | $ brew install eccodes 74 | 75 | Or if you manage binary packages with *Conda* but use Python bindings from elsewhere, use:: 76 | 77 | $ conda install -c conda-forge eccodes 78 | 79 | As an alternative you may install the official source distribution 80 | by following the instructions at 81 | https://confluence.ecmwf.int/display/ECC/ecCodes+installation 82 | 83 | You may run a simple selfcheck command to ensure that your system is set up correctly:: 84 | 85 | $ python -m eccodes selfcheck 86 | Found: ecCodes v2.39.0. 87 | Your system is ready. 88 | 89 | 90 | Debugging the library search 91 | ---------------------------- 92 | 93 | In order to gain insights into the search for the binary library, set the following environment variable before 94 | importing eccodes:: 95 | 96 | $ export ECCODES_PYTHON_TRACE_LIB_SEARCH=1 97 | 98 | 99 | Usage 100 | ----- 101 | 102 | Refer to the *ecCodes* `documentation pages `_ 103 | for usage. 104 | 105 | 106 | Experimental features 107 | ===================== 108 | 109 | Fast bindings 110 | ------------- 111 | 112 | To test the much faster *CFFI* API level, out-of-line mode you need the 113 | *ecCodes* header files. 114 | Then you need to clone the repo in the same folder as your *ecCodes* 115 | source tree, make a ``pip`` development install and custom compile 116 | the binary bindings:: 117 | 118 | $ git clone https://github.com/ecmwf/eccodes-python 119 | $ cd eccodes-python 120 | $ pip install -e . 121 | $ python builder.py 122 | 123 | To revert back to ABI level, in-line mode just remove the compiled bindings:: 124 | 125 | $ rm gribapi/_bindings.* 126 | 127 | 128 | Project resources 129 | ================= 130 | 131 | ============= ========================================================= 132 | Development https://github.com/ecmwf/eccodes-python 133 | Download https://pypi.org/project/eccodes 134 | ============= ========================================================= 135 | 136 | 137 | Contributing 138 | ============ 139 | 140 | The main repository is hosted on GitHub, 141 | testing, bug reports and contributions are highly welcomed and appreciated: 142 | 143 | https://github.com/ecmwf/eccodes-python 144 | 145 | Please see the CONTRIBUTING.rst document for the best way to help. 146 | 147 | Maintainers: 148 | 149 | - `Shahram Najm `_ - `ECMWF `_ 150 | - `Eugen Betke `_ - `ECMWF `_ 151 | 152 | Contributors: 153 | 154 | - `Iain Russell `_ - `ECMWF `_ 155 | - `Alessandro Amici `_ - `B-Open `_ 156 | 157 | See also the list of other `contributors `_ 158 | who participated in this project. 159 | 160 | .. |copy| unicode:: U+000A9 .. COPYRIGHT SIGN 161 | 162 | License 163 | ======= 164 | 165 | |copy| Copyright 2017- ECMWF. 166 | 167 | This software is licensed under the terms of the Apache Licence Version 2.0 168 | which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 169 | 170 | In applying this licence, ECMWF does not waive the privileges and immunities 171 | granted to it by virtue of its status as an intergovernmental organisation nor 172 | does it submit to any jurisdiction. 173 | -------------------------------------------------------------------------------- /builder.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | import cffi 5 | 6 | ffibuilder = cffi.FFI() 7 | ffibuilder.set_source( 8 | "gribapi._bindings", 9 | "#include ", 10 | libraries=["eccodes"], 11 | ) 12 | ffibuilder.cdef(open("gribapi/grib_api.h").read() + open("gribapi/eccodes.h").read()) 13 | 14 | if __name__ == "__main__": 15 | try: 16 | ffibuilder.compile(verbose=True) 17 | except Exception: 18 | logging.exception("can't compile ecCodes bindings") 19 | sys.exit(1) 20 | -------------------------------------------------------------------------------- /ci/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | check-manifest 2 | detox 3 | IPython 4 | matplotlib 5 | notebook 6 | pip-tools 7 | pyroma 8 | pytest-mypy 9 | setuptools 10 | tox 11 | tox-pyenv 12 | wheel 13 | zest.releaser 14 | black 15 | 16 | -------------------------------------------------------------------------------- /ci/requirements-docs.in: -------------------------------------------------------------------------------- 1 | Sphinx 2 | pytest-runner 3 | 4 | -------------------------------------------------------------------------------- /ci/requirements-docs.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile --output-file ci/requirements-docs.txt setup.py ci/requirements-docs.in 6 | # 7 | alabaster==0.7.12 8 | # via sphinx 9 | babel==2.9.1 10 | # via sphinx 11 | certifi==2024.7.4 12 | # via requests 13 | charset-normalizer==3.3.2 14 | # via requests 15 | docutils==0.14 16 | # via sphinx 17 | idna==3.7 18 | # via requests 19 | imagesize==1.1.0 20 | # via sphinx 21 | jinja2==3.1.6 22 | # via sphinx 23 | markupsafe==2.1.5 24 | # via jinja2 25 | packaging==19.0 26 | # via sphinx 27 | pygments==2.15.0 28 | # via sphinx 29 | pyparsing==2.3.1 30 | # via packaging 31 | pytest-runner==4.4 32 | # via -r requirements-docs.in 33 | pytz==2018.9 34 | # via babel 35 | requests==2.32.2 36 | # via sphinx 37 | six==1.12.0 38 | # via 39 | # packaging 40 | # sphinx 41 | snowballstemmer==1.2.1 42 | # via sphinx 43 | sphinx==1.8.5 44 | # via -r requirements-docs.in 45 | sphinxcontrib-websupport==1.1.0 46 | # via sphinx 47 | urllib3==1.26.19 48 | # via requests 49 | -------------------------------------------------------------------------------- /ci/requirements-tests.in: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | pytest-flakes 4 | pytest-mccabe 5 | pytest-pep8 6 | pytest-runner 7 | -------------------------------------------------------------------------------- /ci/requirements-tests.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile --output-file ci/requirements-tests.txt setup.py ci/requirements-tests.in 6 | # 7 | apipkg==1.5 8 | # via execnet 9 | atomicwrites==1.3.0 10 | # via pytest 11 | attrs==19.1.0 12 | # via pytest 13 | coverage==4.5.3 14 | # via pytest-cov 15 | execnet==1.5.0 16 | # via pytest-cache 17 | mccabe==0.6.1 18 | # via pytest-mccabe 19 | more-itertools==5.0.0 20 | # via pytest 21 | pep8==1.7.1 22 | # via pytest-pep8 23 | pluggy==0.9.0 24 | # via pytest 25 | pyflakes==2.1.1 26 | # via pytest-flakes 27 | pytest==4.3.1 28 | # via 29 | # -r requirements-tests.in 30 | # pytest-cache 31 | # pytest-cov 32 | # pytest-flakes 33 | # pytest-mccabe 34 | # pytest-pep8 35 | pytest-cache==1.0 36 | # via 37 | # pytest-mccabe 38 | # pytest-pep8 39 | pytest-cov==2.6.1 40 | # via -r requirements-tests.in 41 | pytest-flakes==4.0.0 42 | # via -r requirements-tests.in 43 | pytest-mccabe==0.1 44 | # via -r requirements-tests.in 45 | pytest-pep8==1.0.6 46 | # via -r requirements-tests.in 47 | pytest-runner==4.4 48 | # via -r requirements-tests.in 49 | six==1.12.0 50 | # via 51 | # more-itertools 52 | # pytest 53 | -------------------------------------------------------------------------------- /docs/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecmwf/eccodes-python/1b1e4a0231a3a34eeb7a285d5138dfe3b4cd4235/docs/_static/.gitkeep -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | 7 | import pkg_resources 8 | 9 | # Get the project root dir, which is the parent dir of this 10 | cwd = os.getcwd() 11 | project_root = os.path.dirname(cwd) 12 | 13 | # Insert the project root dir as the first element in the PYTHONPATH. 14 | # This lets us ensure that the source package is imported, and that its 15 | # version is used. 16 | sys.path.insert(0, project_root) 17 | 18 | # Add any Sphinx extension module names here, as strings. They can be 19 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 20 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] 21 | 22 | # Add any paths that contain templates here, relative to this directory. 23 | templates_path = ["_templates"] 24 | 25 | # The suffix of source filenames. 26 | source_suffix = ".rst" 27 | 28 | # The encoding of source files. 29 | # source_encoding = 'utf-8-sig' 30 | 31 | # The master toctree document. 32 | master_doc = "index" 33 | 34 | # General information about the project. 35 | project = "eccodes-python" 36 | copyright = "2017-, European Centre for Medium-Range Weather Forecasts (ECMWF)." 37 | 38 | # The version info for the project you're documenting, acts as replacement 39 | # for |version| and |release|, also used in various other places throughout 40 | # the built documents. 41 | # 42 | # The full version, including alpha/beta/rc tags. 43 | release = pkg_resources.get_distribution("eccodes-python").version 44 | # The short X.Y version. 45 | version = ".".join(release.split(".")[:2]) 46 | 47 | # The language for content autogenerated by Sphinx. Refer to documentation 48 | # for a list of supported languages. 49 | # language = None 50 | 51 | # There are two options for replacing |today|: either, you set today to 52 | # some non-false value, then it is used: 53 | # today = '' 54 | # Else, today_fmt is used as the format for a strftime call. 55 | # today_fmt = '%B %d, %Y' 56 | 57 | # List of patterns, relative to source directory, that match files and 58 | # directories to ignore when looking for source files. 59 | exclude_patterns = ["_build"] 60 | 61 | # The reST default role (used for this markup: `text`) to use for all 62 | # documents. 63 | # default_role = None 64 | 65 | # If true, '()' will be appended to :func: etc. cross-reference text. 66 | # add_function_parentheses = True 67 | 68 | # If true, the current module name will be prepended to all description 69 | # unit titles (such as .. function::). 70 | # add_module_names = True 71 | 72 | # If true, sectionauthor and moduleauthor directives will be shown in the 73 | # output. They are ignored by default. 74 | # show_authors = False 75 | 76 | # The name of the Pygments (syntax highlighting) style to use. 77 | pygments_style = "sphinx" 78 | 79 | # A list of ignored prefixes for module index sorting. 80 | # modindex_common_prefix = [] 81 | 82 | # If true, keep warnings as "system message" paragraphs in the built 83 | # documents. 84 | # keep_warnings = False 85 | 86 | 87 | # -- Options for HTML output ------------------------------------------- 88 | 89 | # The theme to use for HTML and HTML Help pages. See the documentation for 90 | # a list of builtin themes. 91 | html_theme = "default" 92 | 93 | # Theme options are theme-specific and customize the look and feel of a 94 | # theme further. For a list of options available for each theme, see the 95 | # documentation. 96 | # html_theme_options = {} 97 | 98 | # Add any paths that contain custom themes here, relative to this directory. 99 | # html_theme_path = [] 100 | 101 | # The name for this set of Sphinx documents. If None, it defaults to 102 | # " v documentation". 103 | # html_title = None 104 | 105 | # A shorter title for the navigation bar. Default is the same as 106 | # html_title. 107 | # html_short_title = None 108 | 109 | # The name of an image file (relative to this directory) to place at the 110 | # top of the sidebar. 111 | # html_logo = None 112 | 113 | # The name of an image file (within the static path) to use as favicon 114 | # of the docs. This file should be a Windows icon file (.ico) being 115 | # 16x16 or 32x32 pixels large. 116 | # html_favicon = None 117 | 118 | # Add any paths that contain custom static files (such as style sheets) 119 | # here, relative to this directory. They are copied after the builtin 120 | # static files, so a file named "default.css" will overwrite the builtin 121 | # "default.css". 122 | html_static_path = ["_static"] 123 | 124 | # If not '', a 'Last updated on:' timestamp is inserted at every page 125 | # bottom, using the given strftime format. 126 | # html_last_updated_fmt = '%b %d, %Y' 127 | 128 | # If true, SmartyPants will be used to convert quotes and dashes to 129 | # typographically correct entities. 130 | # html_use_smartypants = True 131 | 132 | # Custom sidebar templates, maps document names to template names. 133 | # html_sidebars = {} 134 | 135 | # Additional templates that should be rendered to pages, maps page names 136 | # to template names. 137 | # html_additional_pages = {} 138 | 139 | # If false, no module index is generated. 140 | # html_domain_indices = True 141 | 142 | # If false, no index is generated. 143 | # html_use_index = True 144 | 145 | # If true, the index is split into individual pages for each letter. 146 | # html_split_index = False 147 | 148 | # If true, links to the reST sources are added to the pages. 149 | # html_show_sourcelink = True 150 | 151 | # If true, "Created using Sphinx" is shown in the HTML footer. 152 | # Default is True. 153 | # html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. 156 | # Default is True. 157 | # html_show_copyright = True 158 | 159 | # If true, an OpenSearch description file will be output, and all pages 160 | # will contain a tag referring to it. The value of this option 161 | # must be the base URL from which the finished HTML is served. 162 | # html_use_opensearch = '' 163 | 164 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 165 | # html_file_suffix = None 166 | 167 | # Output file base name for HTML help builder. 168 | htmlhelp_basename = "cfgribdoc" 169 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | 3 | ======= 4 | CF-GRIB 5 | ======= 6 | 7 | :Version: |release| 8 | :Date: |today| 9 | 10 | 11 | Python 3 interface to encode and decode GRIB and BUFR files via the 12 | `ECMWF ecCodes library `_. 13 | 14 | Features: 15 | 16 | - reads and writes GRIB 1 and 2 files, 17 | - reads and writes BUFR 3 and 4 files, 18 | - supports all modern versions of Python and PyPy3, 19 | - works on most *Linux* distributions and *MacOS*, the *ecCodes* C-library is the only system dependency, 20 | - PyPI package can be installed without compiling, 21 | at the cost of being twice as slow as the original *ecCodes* module, 22 | - an optional compile step makes the code as fast as the original module 23 | but it needs the recommended (the most up-to-date) version of *ecCodes*. 24 | 25 | Limitations: 26 | 27 | - Microsoft Windows support is untested. 28 | 29 | 30 | .. toctree:: 31 | :maxdepth: 2 32 | :caption: Table of Contents 33 | 34 | -------------------------------------------------------------------------------- /eccodes-python.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } 9 | -------------------------------------------------------------------------------- /eccodes/.gitignore: -------------------------------------------------------------------------------- 1 | *.txt 2 | *.json 3 | -------------------------------------------------------------------------------- /eccodes/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # (C) Copyright 2017- ECMWF. 3 | # 4 | # This software is licensed under the terms of the Apache Licence Version 2.0 5 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # In applying this licence, ECMWF does not waive the privileges and immunities 8 | # granted to it by virtue of its status as an intergovernmental organisation nor 9 | # does it submit to any jurisdiction. 10 | # 11 | # 12 | 13 | from .eccodes import * # noqa 14 | from .highlevel import * # noqa 15 | -------------------------------------------------------------------------------- /eccodes/__main__.py: -------------------------------------------------------------------------------- 1 | # 2 | # (C) Copyright 2017- ECMWF. 3 | # 4 | # This software is licensed under the terms of the Apache Licence Version 2.0 5 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # In applying this licence, ECMWF does not waive the privileges and immunities 8 | # granted to it by virtue of its status as an intergovernmental organisation nor 9 | # does it submit to any jurisdiction. 10 | # 11 | 12 | import argparse 13 | 14 | from . import ( 15 | codes_definition_path, 16 | codes_get_api_version, 17 | codes_get_library_path, 18 | codes_samples_path, 19 | ) 20 | 21 | 22 | def selfcheck(): 23 | print("Found: ecCodes v%s." % codes_get_api_version()) 24 | print("Library:", codes_get_library_path()) 25 | print("Definitions:", codes_definition_path()) 26 | print("Samples:", codes_samples_path()) 27 | print("Your system is ready.") 28 | 29 | 30 | def main(argv=None): 31 | parser = argparse.ArgumentParser() 32 | parser.add_argument("command") 33 | args = parser.parse_args(args=argv) 34 | if args.command == "selfcheck": 35 | selfcheck() 36 | else: 37 | raise RuntimeError( 38 | "Command not recognised %r. See usage with --help." % args.command 39 | ) 40 | 41 | 42 | if __name__ == "__main__": # pragma: no cover 43 | main() 44 | -------------------------------------------------------------------------------- /eccodes/_eccodes.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2024- ECMWF. 3 | * 4 | * This software is licensed under the terms of the Apache Licence Version 2.0 5 | * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | * 7 | * In applying this licence, ECMWF does not waive the privileges and immunities granted to it by 8 | * virtue of its status as an intergovernmental organisation nor does it submit to any jurisdiction. 9 | */ 10 | 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | static PyObject *versions(PyObject *self, PyObject *args) 17 | { 18 | long s = grib_get_api_version(); // Force linking 19 | 20 | return Py_BuildValue("{s:s}", 21 | "eccodes", ECCODES_VERSION_STR); 22 | } 23 | 24 | static PyMethodDef eccodes_methods[] = { 25 | { 26 | "versions", 27 | versions, 28 | METH_NOARGS, 29 | "Versions", 30 | }, 31 | { 32 | 0, 33 | }}; 34 | 35 | static struct PyModuleDef eccodes_definition = { 36 | PyModuleDef_HEAD_INIT, 37 | "eccodes", 38 | "Load ecCodes library.", 39 | -1, 40 | eccodes_methods}; 41 | 42 | PyMODINIT_FUNC PyInit__eccodes(void) 43 | { 44 | Py_Initialize(); 45 | return PyModule_Create(&eccodes_definition); 46 | } 47 | -------------------------------------------------------------------------------- /eccodes/copying/.gitignore: -------------------------------------------------------------------------------- 1 | *.txt 2 | *.json 3 | -------------------------------------------------------------------------------- /eccodes/eccodes.py: -------------------------------------------------------------------------------- 1 | # 2 | # (C) Copyright 2017- ECMWF. 3 | # 4 | # This software is licensed under the terms of the Apache Licence Version 2.0 5 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # In applying this licence, ECMWF does not waive the privileges and immunities 8 | # granted to it by virtue of its status as an intergovernmental organisation nor 9 | # does it submit to any jurisdiction. 10 | # 11 | # 12 | from gribapi import ( 13 | CODES_FEATURES_ALL, 14 | CODES_FEATURES_DISABLED, 15 | CODES_FEATURES_ENABLED, 16 | CODES_PRODUCT_ANY, 17 | CODES_PRODUCT_BUFR, 18 | CODES_PRODUCT_GRIB, 19 | CODES_PRODUCT_GTS, 20 | CODES_PRODUCT_METAR, 21 | ) 22 | from gribapi import GRIB_CHECK as CODES_CHECK 23 | from gribapi import GRIB_MISSING_DOUBLE as CODES_MISSING_DOUBLE 24 | from gribapi import GRIB_MISSING_LONG as CODES_MISSING_LONG 25 | from gribapi import GRIB_NEAREST_SAME_DATA as CODES_GRIB_NEAREST_SAME_DATA 26 | from gribapi import GRIB_NEAREST_SAME_GRID as CODES_GRIB_NEAREST_SAME_GRID 27 | from gribapi import GRIB_NEAREST_SAME_POINT as CODES_GRIB_NEAREST_SAME_POINT 28 | from gribapi import ( 29 | __version__, 30 | ) 31 | from gribapi import any_new_from_file as codes_any_new_from_file 32 | from gribapi import ( 33 | bindings_version, 34 | ) 35 | from gribapi import bufr_new_from_file as codes_bufr_new_from_file 36 | from gribapi import ( 37 | codes_any_new_from_samples, 38 | codes_bufr_copy_data, 39 | codes_bufr_extract_headers, 40 | codes_bufr_key_is_coordinate, 41 | codes_bufr_key_is_header, 42 | codes_bufr_keys_iterator_delete, 43 | codes_bufr_keys_iterator_get_name, 44 | codes_bufr_keys_iterator_new, 45 | codes_bufr_keys_iterator_next, 46 | codes_bufr_keys_iterator_rewind, 47 | codes_bufr_multi_element_constant_arrays_off, 48 | codes_bufr_multi_element_constant_arrays_on, 49 | codes_bufr_new_from_samples, 50 | codes_definition_path, 51 | codes_dump, 52 | codes_extract_offsets, 53 | codes_extract_offsets_sizes, 54 | codes_get_features, 55 | codes_get_gaussian_latitudes, 56 | codes_get_library_path, 57 | codes_get_version_info, 58 | codes_new_from_file, 59 | codes_new_from_samples, 60 | codes_samples_path, 61 | ) 62 | from gribapi import grib_clone as codes_clone 63 | from gribapi import grib_context_delete as codes_context_delete 64 | from gribapi import grib_copy_namespace as codes_copy_namespace 65 | from gribapi import grib_count_in_file as codes_count_in_file 66 | from gribapi import grib_find_nearest as codes_grib_find_nearest 67 | from gribapi import grib_find_nearest_multiple as codes_grib_find_nearest_multiple 68 | from gribapi import grib_get as codes_get 69 | from gribapi import grib_get_api_version as codes_get_api_version 70 | from gribapi import grib_get_array as codes_get_array 71 | from gribapi import grib_get_data as codes_grib_get_data 72 | from gribapi import grib_get_double as codes_get_double 73 | from gribapi import grib_get_double_array as codes_get_double_array 74 | from gribapi import grib_get_double_element as codes_get_double_element 75 | from gribapi import grib_get_double_elements as codes_get_double_elements 76 | from gribapi import grib_get_elements as codes_get_elements 77 | from gribapi import grib_get_float_array as codes_get_float_array 78 | from gribapi import grib_get_long as codes_get_long 79 | from gribapi import grib_get_long_array as codes_get_long_array 80 | from gribapi import grib_get_message as codes_get_message 81 | from gribapi import grib_get_message_offset as codes_get_message_offset 82 | from gribapi import grib_get_message_size as codes_get_message_size 83 | from gribapi import grib_get_native_type as codes_get_native_type 84 | from gribapi import grib_get_offset as codes_get_offset 85 | from gribapi import grib_get_size as codes_get_size 86 | from gribapi import grib_get_string as codes_get_string 87 | from gribapi import grib_get_string_array as codes_get_string_array 88 | from gribapi import grib_get_string_length as codes_get_string_length 89 | from gribapi import grib_get_values as codes_get_values 90 | from gribapi import grib_gribex_mode_off as codes_gribex_mode_off 91 | from gribapi import grib_gribex_mode_on as codes_gribex_mode_on 92 | from gribapi import grib_gts_header as codes_gts_header 93 | from gribapi import grib_index_add_file as codes_index_add_file 94 | from gribapi import grib_index_get as codes_index_get 95 | from gribapi import grib_index_get_double as codes_index_get_double 96 | from gribapi import grib_index_get_long as codes_index_get_long 97 | from gribapi import grib_index_get_size as codes_index_get_size 98 | from gribapi import grib_index_get_string as codes_index_get_string 99 | from gribapi import grib_index_new_from_file as codes_index_new_from_file 100 | from gribapi import grib_index_read as codes_index_read 101 | from gribapi import grib_index_release as codes_index_release 102 | from gribapi import grib_index_select as codes_index_select 103 | from gribapi import grib_index_select_double as codes_index_select_double 104 | from gribapi import grib_index_select_long as codes_index_select_long 105 | from gribapi import grib_index_select_string as codes_index_select_string 106 | from gribapi import grib_index_write as codes_index_write 107 | from gribapi import grib_is_defined as codes_is_defined 108 | from gribapi import grib_is_missing as codes_is_missing 109 | from gribapi import grib_iterator_delete as codes_grib_iterator_delete 110 | from gribapi import grib_iterator_new as codes_grib_iterator_new 111 | from gribapi import grib_iterator_next as codes_grib_iterator_next 112 | from gribapi import grib_keys_iterator_delete as codes_keys_iterator_delete 113 | from gribapi import grib_keys_iterator_get_name as codes_keys_iterator_get_name 114 | from gribapi import grib_keys_iterator_new as codes_keys_iterator_new 115 | from gribapi import grib_keys_iterator_next as codes_keys_iterator_next 116 | from gribapi import grib_keys_iterator_rewind as codes_keys_iterator_rewind 117 | from gribapi import grib_multi_append as codes_grib_multi_append 118 | from gribapi import grib_multi_new as codes_grib_multi_new 119 | from gribapi import grib_multi_release as codes_grib_multi_release 120 | from gribapi import grib_multi_support_off as codes_grib_multi_support_off 121 | from gribapi import grib_multi_support_on as codes_grib_multi_support_on 122 | from gribapi import grib_multi_support_reset_file as codes_grib_multi_support_reset_file 123 | from gribapi import grib_multi_write as codes_grib_multi_write 124 | from gribapi import grib_nearest_delete as codes_grib_nearest_delete 125 | from gribapi import grib_nearest_find as codes_grib_nearest_find 126 | from gribapi import grib_nearest_new as codes_grib_nearest_new 127 | from gribapi import grib_new_from_file as codes_grib_new_from_file 128 | from gribapi import grib_new_from_index as codes_new_from_index 129 | from gribapi import grib_new_from_message as codes_new_from_message 130 | from gribapi import grib_new_from_samples as codes_grib_new_from_samples 131 | from gribapi import grib_no_fail_on_wrong_length as codes_no_fail_on_wrong_length 132 | from gribapi import grib_release as codes_release 133 | from gribapi import grib_set as codes_set 134 | from gribapi import grib_set_array as codes_set_array 135 | from gribapi import grib_set_data_quality_checks as codes_set_data_quality_checks 136 | from gribapi import grib_set_debug as codes_set_debug 137 | from gribapi import grib_set_definitions_path as codes_set_definitions_path 138 | from gribapi import grib_set_double as codes_set_double 139 | from gribapi import grib_set_double_array as codes_set_double_array 140 | from gribapi import grib_set_key_vals as codes_set_key_vals 141 | from gribapi import grib_set_long as codes_set_long 142 | from gribapi import grib_set_long_array as codes_set_long_array 143 | from gribapi import grib_set_missing as codes_set_missing 144 | from gribapi import grib_set_samples_path as codes_set_samples_path 145 | from gribapi import grib_set_string as codes_set_string 146 | from gribapi import grib_set_string_array as codes_set_string_array 147 | from gribapi import grib_set_values as codes_set_values 148 | from gribapi import grib_skip_coded as codes_skip_coded 149 | from gribapi import grib_skip_computed as codes_skip_computed 150 | from gribapi import grib_skip_duplicates as codes_skip_duplicates 151 | from gribapi import grib_skip_edition_specific as codes_skip_edition_specific 152 | from gribapi import grib_skip_function as codes_skip_function 153 | from gribapi import grib_skip_read_only as codes_skip_read_only 154 | from gribapi import grib_write as codes_write 155 | from gribapi import gts_new_from_file as codes_gts_new_from_file 156 | from gribapi import metar_new_from_file as codes_metar_new_from_file 157 | from gribapi.errors import ( 158 | ArrayTooSmallError, 159 | AttributeClashError, 160 | AttributeNotFoundError, 161 | BufferTooSmallError, 162 | CodeNotFoundInTableError, 163 | ConceptNoMatchError, 164 | ConstantFieldError, 165 | CorruptedIndexError, 166 | DecodingError, 167 | DifferentEditionError, 168 | EncodingError, 169 | EndError, 170 | EndOfFileError, 171 | EndOfIndexError, 172 | FileNotFoundError, 173 | FunctionalityNotEnabledError, 174 | FunctionNotImplementedError, 175 | GeocalculusError, 176 | ) 177 | from gribapi.errors import GribInternalError 178 | from gribapi.errors import GribInternalError as CodesInternalError 179 | from gribapi.errors import ( 180 | HashArrayNoMatchError, 181 | InternalArrayTooSmallError, 182 | InternalError, 183 | InvalidArgumentError, 184 | InvalidBitsPerValueError, 185 | InvalidFileError, 186 | InvalidGribError, 187 | InvalidIndexError, 188 | InvalidIteratorError, 189 | InvalidKeysIteratorError, 190 | InvalidKeyValueError, 191 | InvalidNearestError, 192 | InvalidOrderByError, 193 | InvalidSectionNumberError, 194 | InvalidTypeError, 195 | IOProblemError, 196 | KeyValueNotFoundError, 197 | MemoryAllocationError, 198 | MessageEndNotFoundError, 199 | MessageInvalidError, 200 | MessageMalformedError, 201 | MessageTooLargeError, 202 | MissingBufrEntryError, 203 | MissingKeyError, 204 | NoDefinitionsError, 205 | NoMoreInSetError, 206 | NoValuesError, 207 | NullHandleError, 208 | NullIndexError, 209 | NullPointerError, 210 | OutOfAreaError, 211 | OutOfRangeError, 212 | PrematureEndOfFileError, 213 | ReadOnlyError, 214 | StringTooSmallError, 215 | SwitchNoMatchError, 216 | TooManyAttributesError, 217 | UnderflowError, 218 | UnsupportedEditionError, 219 | ValueCannotBeMissingError, 220 | ValueDifferentError, 221 | WrongArraySizeError, 222 | WrongBitmapSizeError, 223 | WrongConversionError, 224 | WrongGridError, 225 | WrongLengthError, 226 | WrongStepError, 227 | WrongStepUnitError, 228 | WrongTypeError, 229 | ) 230 | 231 | __all__ = [ 232 | "__version__", 233 | "ArrayTooSmallError", 234 | "AttributeClashError", 235 | "AttributeNotFoundError", 236 | "bindings_version", 237 | "BufferTooSmallError", 238 | "CodeNotFoundInTableError", 239 | "codes_any_new_from_file", 240 | "codes_bufr_copy_data", 241 | "codes_bufr_extract_headers", 242 | "codes_bufr_key_is_header", 243 | "codes_bufr_key_is_coordinate", 244 | "codes_bufr_keys_iterator_delete", 245 | "codes_bufr_keys_iterator_get_name", 246 | "codes_bufr_keys_iterator_new", 247 | "codes_bufr_keys_iterator_next", 248 | "codes_bufr_keys_iterator_rewind", 249 | "codes_bufr_multi_element_constant_arrays_off", 250 | "codes_bufr_multi_element_constant_arrays_on", 251 | "codes_bufr_new_from_file", 252 | "codes_bufr_new_from_samples", 253 | "codes_any_new_from_samples", 254 | "CODES_CHECK", 255 | "codes_clone", 256 | "codes_copy_namespace", 257 | "codes_count_in_file", 258 | "codes_definition_path", 259 | "codes_extract_offsets", 260 | "codes_extract_offsets_sizes", 261 | "CODES_FEATURES_ALL", 262 | "CODES_FEATURES_ENABLED", 263 | "CODES_FEATURES_DISABLED", 264 | "codes_get_api_version", 265 | "codes_get_array", 266 | "codes_get_double_array", 267 | "codes_get_double_element", 268 | "codes_get_double_elements", 269 | "codes_get_double", 270 | "codes_get_elements", 271 | "codes_get_float_array", 272 | "codes_get_gaussian_latitudes", 273 | "codes_get_library_path", 274 | "codes_get_long_array", 275 | "codes_get_long", 276 | "codes_get_message_offset", 277 | "codes_get_message_size", 278 | "codes_get_message", 279 | "codes_get_native_type", 280 | "codes_get_offset", 281 | "codes_get_size", 282 | "codes_get_string_array", 283 | "codes_get_string_length", 284 | "codes_get_string", 285 | "codes_get_values", 286 | "codes_get_version_info", 287 | "codes_get", 288 | "codes_get_features", 289 | "codes_grib_find_nearest_multiple", 290 | "codes_grib_find_nearest", 291 | "codes_grib_get_data", 292 | "codes_grib_iterator_delete", 293 | "codes_grib_iterator_new", 294 | "codes_grib_iterator_next", 295 | "codes_grib_multi_append", 296 | "codes_grib_multi_new", 297 | "codes_grib_multi_release", 298 | "codes_grib_multi_support_off", 299 | "codes_grib_multi_support_on", 300 | "codes_grib_multi_support_reset_file", 301 | "codes_grib_multi_write", 302 | "codes_grib_nearest_delete", 303 | "codes_grib_nearest_find", 304 | "codes_grib_nearest_new", 305 | "CODES_GRIB_NEAREST_SAME_DATA", 306 | "CODES_GRIB_NEAREST_SAME_GRID", 307 | "CODES_GRIB_NEAREST_SAME_POINT", 308 | "codes_grib_new_from_file", 309 | "codes_grib_new_from_samples", 310 | "codes_gribex_mode_off", 311 | "codes_gribex_mode_on", 312 | "codes_gts_header", 313 | "codes_gts_new_from_file", 314 | "codes_index_add_file", 315 | "codes_index_get_double", 316 | "codes_index_get_long", 317 | "codes_index_get_size", 318 | "codes_index_get_string", 319 | "codes_index_get", 320 | "codes_index_new_from_file", 321 | "codes_index_read", 322 | "codes_index_release", 323 | "codes_index_select_double", 324 | "codes_index_select_long", 325 | "codes_index_select_string", 326 | "codes_index_select", 327 | "codes_index_write", 328 | "codes_is_defined", 329 | "codes_is_missing", 330 | "codes_keys_iterator_delete", 331 | "codes_keys_iterator_get_name", 332 | "codes_keys_iterator_new", 333 | "codes_keys_iterator_next", 334 | "codes_keys_iterator_rewind", 335 | "codes_metar_new_from_file", 336 | "CODES_MISSING_DOUBLE", 337 | "CODES_MISSING_LONG", 338 | "codes_new_from_file", 339 | "codes_new_from_index", 340 | "codes_new_from_message", 341 | "codes_new_from_samples", 342 | "codes_no_fail_on_wrong_length", 343 | "CODES_PRODUCT_ANY", 344 | "CODES_PRODUCT_BUFR", 345 | "CODES_PRODUCT_GRIB", 346 | "CODES_PRODUCT_GTS", 347 | "CODES_PRODUCT_METAR", 348 | "codes_release", 349 | "codes_samples_path", 350 | "codes_dump", 351 | "codes_set_array", 352 | "codes_set_data_quality_checks", 353 | "codes_set_debug", 354 | "codes_set_definitions_path", 355 | "codes_set_double_array", 356 | "codes_set_double", 357 | "codes_set_key_vals", 358 | "codes_set_long_array", 359 | "codes_set_long", 360 | "codes_set_missing", 361 | "codes_set_samples_path", 362 | "codes_set_string_array", 363 | "codes_set_string", 364 | "codes_set_values", 365 | "codes_set", 366 | "codes_skip_coded", 367 | "codes_skip_computed", 368 | "codes_skip_duplicates", 369 | "codes_skip_edition_specific", 370 | "codes_skip_function", 371 | "codes_skip_read_only", 372 | "codes_write", 373 | "codes_context_delete", 374 | "CodesInternalError", 375 | "ConceptNoMatchError", 376 | "ConstantFieldError", 377 | "CorruptedIndexError", 378 | "DecodingError", 379 | "DifferentEditionError", 380 | "EncodingError", 381 | "EndError", 382 | "EndOfFileError", 383 | "EndOfIndexError", 384 | "FileNotFoundError", 385 | "FunctionalityNotEnabledError", 386 | "FunctionNotImplementedError", 387 | "GeocalculusError", 388 | "GribInternalError", 389 | "HashArrayNoMatchError", 390 | "InternalArrayTooSmallError", 391 | "InternalError", 392 | "InvalidArgumentError", 393 | "InvalidBitsPerValueError", 394 | "InvalidFileError", 395 | "InvalidGribError", 396 | "InvalidIndexError", 397 | "InvalidIteratorError", 398 | "InvalidKeysIteratorError", 399 | "InvalidKeyValueError", 400 | "InvalidNearestError", 401 | "InvalidOrderByError", 402 | "InvalidSectionNumberError", 403 | "InvalidTypeError", 404 | "IOProblemError", 405 | "KeyValueNotFoundError", 406 | "MemoryAllocationError", 407 | "MessageEndNotFoundError", 408 | "MessageInvalidError", 409 | "MessageMalformedError", 410 | "MessageTooLargeError", 411 | "MissingBufrEntryError", 412 | "MissingKeyError", 413 | "NoDefinitionsError", 414 | "NoMoreInSetError", 415 | "NoValuesError", 416 | "NullHandleError", 417 | "NullIndexError", 418 | "NullPointerError", 419 | "OutOfAreaError", 420 | "OutOfRangeError", 421 | "PrematureEndOfFileError", 422 | "ReadOnlyError", 423 | "StringTooSmallError", 424 | "SwitchNoMatchError", 425 | "TooManyAttributesError", 426 | "UnderflowError", 427 | "UnsupportedEditionError", 428 | "ValueCannotBeMissingError", 429 | "ValueDifferentError", 430 | "WrongArraySizeError", 431 | "WrongBitmapSizeError", 432 | "WrongConversionError", 433 | "WrongGridError", 434 | "WrongLengthError", 435 | "WrongStepError", 436 | "WrongStepUnitError", 437 | "WrongTypeError", 438 | ] 439 | -------------------------------------------------------------------------------- /eccodes/highlevel/__init__.py: -------------------------------------------------------------------------------- 1 | from .message import BUFRMessage, GRIBMessage, Message # noqa 2 | from .reader import FileReader, MemoryReader, StreamReader # noqa 3 | -------------------------------------------------------------------------------- /eccodes/highlevel/message.py: -------------------------------------------------------------------------------- 1 | import io 2 | from contextlib import contextmanager 3 | 4 | import numpy as np 5 | 6 | import eccodes 7 | 8 | _TYPES_MAP = { 9 | "float": float, 10 | "int": int, 11 | "str": str, 12 | } 13 | 14 | 15 | @contextmanager 16 | def raise_keyerror(name): 17 | """Make operations on a key raise a KeyError if not found""" 18 | try: 19 | yield 20 | except (eccodes.KeyValueNotFoundError, eccodes.FunctionNotImplementedError): 21 | raise KeyError(name) 22 | 23 | 24 | class Message: 25 | def __init__(self, handle): 26 | self._handle = handle 27 | 28 | def __del__(self): 29 | try: 30 | eccodes.codes_release(self._handle) 31 | except Exception: 32 | pass 33 | 34 | def copy(self): 35 | """Create a copy of the current message""" 36 | return self.__class__(eccodes.codes_clone(self._handle)) 37 | 38 | def __copy__(self): 39 | return self.copy() 40 | 41 | def _get(self, name, ktype=None): 42 | name, sep, stype = name.partition(":") 43 | if sep and ktype is None: 44 | try: 45 | ktype = _TYPES_MAP[stype] 46 | except KeyError: 47 | raise ValueError(f"Unknown key type {stype!r}") 48 | with raise_keyerror(name): 49 | if eccodes.codes_is_missing(self._handle, name): 50 | raise KeyError(name) 51 | if eccodes.codes_get_size(self._handle, name) > 1: 52 | return eccodes.codes_get_array(self._handle, name, ktype=ktype) 53 | return eccodes.codes_get(self._handle, name, ktype=ktype) 54 | 55 | def get(self, name, default=None, ktype=None): 56 | """Get the value of a key 57 | 58 | Parameters 59 | ---------- 60 | name: str 61 | Name of the key. Can be suffixed with ":str", ":int", or ":float" to 62 | request a specific type. 63 | default: any, optional 64 | Value if the key is not Found, or ``None`` if not specified. 65 | ktype: type 66 | Request a specific type for the value. Overrides the suffix in ``name``""" 67 | try: 68 | return self._get(name, ktype=ktype) 69 | except KeyError: 70 | return default 71 | 72 | def set(self, *args, check_values: bool = True): 73 | """If two arguments are given, assumes this takes form of a single key 74 | value pair and sets the value of the given key. If a dictionary is passed in, 75 | then sets the values of all keys in the dictionary. Note, ordering 76 | of the keys is important. Finally, by default, checks if values 77 | have been set correctly 78 | 79 | Raises 80 | ------ 81 | TypeError 82 | If arguments do not take one of the two expected forms 83 | KeyError 84 | If the key does not exist 85 | ValueError 86 | If the set value of one of the keys is not the expected value 87 | """ 88 | if isinstance(args[0], str) and len(args) == 2: 89 | key_values = {args[0]: args[1]} 90 | elif isinstance(args[0], dict): 91 | key_values = args[0] 92 | else: 93 | raise TypeError( 94 | "Unsupported argument type. Expects two arguments consisting \ 95 | of key and value pair, or a dictionary of key-value pairs" 96 | ) 97 | 98 | for name, value in key_values.items(): 99 | with raise_keyerror(name): 100 | if np.ndim(value) > 0: 101 | eccodes.codes_set_array(self._handle, name, value) 102 | else: 103 | eccodes.codes_set(self._handle, name, value) 104 | 105 | if check_values: 106 | # Check values just set 107 | for name, value in key_values.items(): 108 | if type(value) in _TYPES_MAP.values(): 109 | saved_value = self.get(f"{name}:{type(value).__name__}") 110 | else: 111 | saved_value = self.get(name) 112 | if not np.all(saved_value == value): 113 | raise ValueError( 114 | f"Unexpected retrieved value {saved_value} for key {name}. Expected {value}" 115 | ) 116 | 117 | def get_array(self, name): 118 | """Get the value of the given key as an array 119 | 120 | Raises 121 | ------ 122 | KeyError 123 | If the key is not set 124 | """ 125 | with raise_keyerror(name): 126 | return eccodes.codes_get_array(self._handle, name) 127 | 128 | def get_size(self, name): 129 | """Get the size of the given key 130 | 131 | Raises 132 | ------ 133 | KeyError 134 | If the key is not set 135 | """ 136 | with raise_keyerror(name): 137 | return eccodes.codes_get_size(self._handle, name) 138 | 139 | def get_data_points(self): 140 | raise NotImplementedError 141 | 142 | def is_missing(self, name): 143 | """Check whether the key is set to a missing value 144 | 145 | Raises 146 | ------ 147 | KeyError 148 | If the key is not set 149 | """ 150 | with raise_keyerror(name): 151 | return bool(eccodes.codes_is_missing(self._handle, name)) 152 | 153 | def set_array(self, name, value): 154 | """Set the value of the given key 155 | 156 | Raises 157 | ------ 158 | KeyError 159 | If the key does not exist 160 | """ 161 | with raise_keyerror(name): 162 | return eccodes.codes_set_array(self._handle, name, value) 163 | 164 | def set_missing(self, name): 165 | """Set the given key as missing 166 | 167 | Raises 168 | ------ 169 | KeyError 170 | If the key does not exist 171 | """ 172 | with raise_keyerror(name): 173 | return eccodes.codes_set_missing(self._handle, name) 174 | 175 | def __getitem__(self, name): 176 | return self._get(name) 177 | 178 | def __setitem__(self, name, value): 179 | self.set(name, value) 180 | 181 | def __contains__(self, name): 182 | return bool(eccodes.codes_is_defined(self._handle, name)) 183 | 184 | class _KeyIterator: 185 | def __init__(self, message, namespace=None, iter_keys=True, iter_values=False): 186 | self._message = message 187 | self._iterator = eccodes.codes_keys_iterator_new(message._handle, namespace) 188 | self._iter_keys = iter_keys 189 | self._iter_values = iter_values 190 | 191 | def __del__(self): 192 | try: 193 | eccodes.codes_keys_iterator_delete(self._iterator) 194 | except Exception: 195 | pass 196 | 197 | def __iter__(self): 198 | return self 199 | 200 | def __next__(self): 201 | while True: 202 | if not eccodes.codes_keys_iterator_next(self._iterator): 203 | raise StopIteration 204 | if not self._iter_keys and not self._iter_values: 205 | return 206 | key = eccodes.codes_keys_iterator_get_name(self._iterator) 207 | if self._message.is_missing(key): 208 | continue 209 | if self._iter_keys and not self._iter_values: 210 | return key 211 | value = self._message.get(key) if self._iter_values else None 212 | if not self._iter_keys: 213 | return value 214 | return key, value 215 | 216 | def __iter__(self): 217 | return self._KeyIterator(self) 218 | 219 | def keys(self, namespace=None): 220 | """Iterate over all the available keys""" 221 | return self._KeyIterator(self, namespace, iter_keys=True, iter_values=False) 222 | 223 | def values(self, namespace=None): 224 | """Iterate over the values of all the available keys""" 225 | return self._KeyIterator(self, namespace, iter_keys=False, iter_values=True) 226 | 227 | def items(self, namespace=None): 228 | """Iterate over all the available key-value pairs""" 229 | return self._KeyIterator(self, namespace, iter_keys=True, iter_values=True) 230 | 231 | def dump(self): 232 | """Print out a textual representation of the message""" 233 | eccodes.codes_dump(self._handle) 234 | 235 | def write_to(self, fileobj): 236 | """Write the message to a file object""" 237 | assert isinstance(fileobj, io.IOBase) 238 | eccodes.codes_write(self._handle, fileobj) 239 | 240 | def get_buffer(self): 241 | """Return a buffer containing the encoded message""" 242 | return eccodes.codes_get_message(self._handle) 243 | 244 | 245 | class BUFRMessage(Message): 246 | def __init__(self, handle): 247 | super().__init__(handle) 248 | 249 | def pack(self): 250 | """Pack the underlying data""" 251 | self.set("pack", 1, check_values=False) 252 | 253 | def unpack(self): 254 | """Unpack the underlying data""" 255 | self.set("unpack", 1, check_values=False) 256 | 257 | @classmethod 258 | def from_samples(cls, name): 259 | """Create a message from a sample""" 260 | return cls(eccodes.codes_bufr_new_from_samples(name)) 261 | 262 | 263 | class GRIBMessage(Message): 264 | def __init__(self, handle): 265 | super().__init__(handle) 266 | self._data = None 267 | 268 | @property 269 | def data(self): 270 | """Return the array of values""" 271 | if self._data is None: 272 | self._data = self._get("values") 273 | return self._data 274 | 275 | def get_data_points(self): 276 | """Get the list of ``(lat, lon, value)`` data points""" 277 | return eccodes.codes_grib_get_data(self._handle) 278 | 279 | @classmethod 280 | def from_samples(cls, name): 281 | """Create a message from a sample""" 282 | return cls(eccodes.codes_grib_new_from_samples(name)) 283 | -------------------------------------------------------------------------------- /eccodes/highlevel/reader.py: -------------------------------------------------------------------------------- 1 | import eccodes 2 | import gribapi 3 | from gribapi import ffi 4 | 5 | from .message import BUFRMessage, GRIBMessage 6 | 7 | _MSG_CLASSES = { 8 | eccodes.CODES_PRODUCT_GRIB: GRIBMessage, 9 | eccodes.CODES_PRODUCT_BUFR: BUFRMessage, 10 | } 11 | 12 | 13 | class ReaderBase: 14 | def __init__(self, kind=eccodes.CODES_PRODUCT_GRIB): 15 | self._peeked = None 16 | self._kind = kind 17 | cls = _MSG_CLASSES.get(kind) 18 | if cls is None: 19 | raise ValueError(f"Unsupported product type {kind}") 20 | self._msg_class = cls 21 | 22 | def __iter__(self): 23 | return self 24 | 25 | def __next__(self): 26 | if self._peeked is not None: 27 | msg = self._peeked 28 | self._peeked = None 29 | return msg 30 | handle = self._next_handle() 31 | if handle is None: 32 | raise StopIteration 33 | return self._msg_class(handle) 34 | 35 | def _next_handle(self): 36 | raise NotImplementedError 37 | 38 | def __enter__(self): 39 | return self 40 | 41 | def __exit__(self, exc_type, exc_value, traceback): 42 | pass 43 | 44 | def peek(self): 45 | """Return the next available message without consuming it""" 46 | if self._peeked is None: 47 | handle = self._next_handle() 48 | if handle is not None: 49 | self._peeked = self._msg_class(handle) 50 | return self._peeked 51 | 52 | 53 | class FileReader(ReaderBase): 54 | """Read messages from a file""" 55 | 56 | def __init__(self, path, kind=eccodes.CODES_PRODUCT_GRIB): 57 | super().__init__(kind=kind) 58 | self.file = open(path, "rb") 59 | 60 | def _next_handle(self): 61 | return eccodes.codes_new_from_file(self.file, self._kind) 62 | 63 | def __enter__(self): 64 | self.file.__enter__() 65 | return self 66 | 67 | def __exit__(self, exc_type, exc_value, traceback): 68 | return self.file.__exit__(exc_type, exc_value, traceback) 69 | 70 | 71 | class MemoryReader(ReaderBase): 72 | """Read messages from memory""" 73 | 74 | def __init__(self, buf, kind=eccodes.CODES_PRODUCT_GRIB): 75 | super().__init__(kind=kind) 76 | self.buf = buf 77 | 78 | def _next_handle(self): 79 | if self.buf is None: 80 | return None 81 | handle = eccodes.codes_new_from_message(self.buf) 82 | self.buf = None 83 | return handle 84 | 85 | 86 | try: 87 | 88 | @ffi.callback("long(*)(void*, void*, long)") 89 | def pyread_callback(payload, buf, length): 90 | stream = ffi.from_handle(payload) 91 | read = stream.read(length) 92 | n = len(read) 93 | ffi.buffer(buf, length)[:n] = read 94 | return n if n > 0 else -1 # -1 means EOF 95 | 96 | except MemoryError: 97 | # ECC-1460 ffi.callback raises a MemoryError if it cannot allocate write+execute memory 98 | pyread_callback = None 99 | 100 | 101 | try: 102 | cstd = ffi.dlopen(None) # Raises OSError on Windows 103 | ffi.cdef("void free(void* pointer);") 104 | ffi.cdef( 105 | "void* wmo_read_any_from_stream_malloc(void*, long (*stream_proc)(void*, void*, long), size_t*, int*);" 106 | ) 107 | except OSError: 108 | cstd = None 109 | 110 | 111 | def codes_new_from_stream(stream): 112 | if cstd is None: 113 | raise OSError("This feature is not supported on Windows") 114 | if pyread_callback is None: 115 | raise OSError( 116 | "This feature cannot be used because the OS prevents allocating write+execute memory" 117 | ) 118 | sh = ffi.new_handle(stream) 119 | length = ffi.new("size_t*") 120 | err = ffi.new("int*") 121 | err, buf = gribapi.err_last(gribapi.lib.wmo_read_any_from_stream_malloc)( 122 | sh, pyread_callback, length 123 | ) 124 | buf = ffi.gc(buf, cstd.free, size=length[0]) 125 | if err: 126 | if err != gribapi.lib.GRIB_END_OF_FILE: 127 | gribapi.GRIB_CHECK(err) 128 | return None 129 | 130 | # TODO: remove the extra copy? 131 | handle = gribapi.lib.grib_handle_new_from_message_copy(ffi.NULL, buf, length[0]) 132 | if handle == ffi.NULL: 133 | return None 134 | else: 135 | return gribapi.put_handle(handle) 136 | 137 | 138 | class StreamReader(ReaderBase): 139 | """Read messages from a stream (an object with a ``read`` method)""" 140 | 141 | def __init__(self, stream, kind=eccodes.CODES_PRODUCT_GRIB): 142 | if cstd is None: 143 | raise OSError("This feature is not supported on Windows") 144 | if pyread_callback is None: 145 | raise OSError( 146 | "This feature cannot be used because the OS prevents allocating write+execute memory" 147 | ) 148 | super().__init__(kind=kind) 149 | self.stream = stream 150 | 151 | def _next_handle(self): 152 | return codes_new_from_stream(self.stream) 153 | -------------------------------------------------------------------------------- /gribapi/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # (C) Copyright 2017- ECMWF. 3 | # 4 | # This software is licensed under the terms of the Apache Licence Version 2.0 5 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # In applying this licence, ECMWF does not waive the privileges and immunities 8 | # granted to it by virtue of its status as an intergovernmental organisation nor 9 | # does it submit to any jurisdiction. 10 | # 11 | # 12 | 13 | from .gribapi import * # noqa 14 | from .gribapi import __version__, lib 15 | 16 | # The minimum recommended version for the ecCodes package 17 | min_recommended_version_str = "2.39.0" 18 | min_recommended_version_int = 23900 19 | 20 | if lib.grib_get_api_version() < min_recommended_version_int: 21 | import warnings 22 | 23 | warnings.warn( 24 | "ecCodes {} or higher is recommended. " 25 | "You are running version {}".format(min_recommended_version_str, __version__) 26 | ) 27 | -------------------------------------------------------------------------------- /gribapi/bindings.py: -------------------------------------------------------------------------------- 1 | # 2 | # (C) Copyright 2017- ECMWF. 3 | # 4 | # This software is licensed under the terms of the Apache Licence Version 2.0 5 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # In applying this licence, ECMWF does not waive the privileges and immunities 8 | # granted to it by virtue of its status as an intergovernmental organisation nor 9 | # does it submit to any jurisdiction. 10 | # 11 | 12 | # Authors: 13 | # Alessandro Amici - B-Open - https://bopen.eu 14 | # Shahram Najm - ECMWF - https://www.ecmwf.int 15 | # 16 | 17 | from __future__ import absolute_import, division, print_function, unicode_literals 18 | 19 | import logging 20 | import os 21 | import pkgutil 22 | import sys 23 | 24 | import cffi 25 | 26 | __version__ = "2.42.0" 27 | 28 | LOG = logging.getLogger(__name__) 29 | 30 | _MAP = { 31 | "grib_api": "eccodes", 32 | "gribapi": "eccodes", 33 | } 34 | 35 | EXTENSIONS = { 36 | "darwin": ".dylib", 37 | "win32": ".dll", 38 | } 39 | 40 | # convenient way to trace the search for the library 41 | if int(os.environ.get("ECCODES_PYTHON_TRACE_LIB_SEARCH", "0")): 42 | LOG.setLevel(logging.DEBUG) 43 | LOG.addHandler(logging.StreamHandler()) 44 | 45 | 46 | def _lookup(name): 47 | return _MAP.get(name, name) 48 | 49 | 50 | def find_binary_libs(name): 51 | name = _lookup(name) 52 | env_var = "ECCODES_PYTHON_USE_FINDLIBS" 53 | if int(os.environ.get(env_var, "0")): 54 | LOG.debug(f"{name} lib search: {env_var} set, so using findlibs") 55 | 56 | else: 57 | LOG.debug(f"{name} lib search: trying to find binary wheel") 58 | here = os.path.dirname(__file__) 59 | # eccodes libs are actually in eccodes dir, not gribapi dir 60 | here = os.path.abspath(os.path.join(here, os.path.pardir, "eccodes")) 61 | extension = EXTENSIONS.get(sys.platform, ".so") 62 | 63 | for libdir in [here + ".libs", os.path.join(here, ".dylibs"), here]: 64 | LOG.debug(f"{name} lib search: looking in {libdir}") 65 | if not name.startswith("lib"): 66 | libnames = ["lib" + name, name] 67 | else: 68 | libnames = [name, name[3:]] 69 | 70 | if os.path.exists(libdir): 71 | for file in os.listdir(libdir): 72 | if file.endswith(extension): 73 | for libname in libnames: 74 | if libname == file.split("-")[0].split(".")[0]: 75 | foundlib = os.path.join(libdir, file) 76 | LOG.debug( 77 | f"{name} lib search: returning wheel library from {foundlib}" 78 | ) 79 | # force linking with the C++ 'glue' library 80 | try: 81 | from eccodes._eccodes import versions as _versions 82 | except ImportError as e: 83 | LOG.warn(str(e)) 84 | raise 85 | LOG.debug( 86 | f"{name} lib search: versions: %s", _versions() 87 | ) 88 | return foundlib 89 | 90 | LOG.debug( 91 | f"{name} lib search: did not find library from wheel; try to find as separate lib" 92 | ) 93 | 94 | # if did not find the binary wheel, or the env var is set, fall back to findlibs 95 | import findlibs 96 | 97 | foundlib = findlibs.find(name) 98 | LOG.debug(f"{name} lib search: findlibs returned {foundlib}") 99 | return foundlib 100 | 101 | 102 | library_path = find_binary_libs("eccodes") 103 | 104 | if library_path is None: 105 | raise RuntimeError("Cannot find the ecCodes library") 106 | 107 | # default encoding for ecCodes strings 108 | ENC = "ascii" 109 | 110 | ffi = cffi.FFI() 111 | CDEF = pkgutil.get_data(__name__, "grib_api.h") 112 | CDEF += pkgutil.get_data(__name__, "eccodes.h") 113 | ffi.cdef(CDEF.decode("utf-8").replace("\r", "\n")) 114 | 115 | 116 | lib = ffi.dlopen(library_path) 117 | -------------------------------------------------------------------------------- /gribapi/eccodes.h: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2017- ECMWF. 3 | * 4 | * This software is licensed under the terms of the Apache Licence Version 2.0 5 | * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | * 7 | * In applying this licence, ECMWF does not waive the privileges and immunities granted to it by 8 | * virtue of its status as an intergovernmental organisation nor does it submit to any jurisdiction. 9 | */ 10 | 11 | typedef struct grib_handle codes_handle; 12 | typedef struct grib_context codes_context; 13 | 14 | grib_handle* codes_handle_new_from_file(codes_context* c, FILE* f, ProductKind product, int* error); 15 | 16 | codes_handle* codes_bufr_handle_new_from_samples(codes_context* c, const char* sample_name); 17 | codes_handle* codes_handle_new_from_samples(codes_context* c, const char* sample_name); 18 | 19 | int codes_bufr_copy_data(grib_handle* hin, grib_handle* hout); 20 | 21 | void codes_bufr_multi_element_constant_arrays_on(codes_context* c); 22 | void codes_bufr_multi_element_constant_arrays_off(codes_context* c); 23 | int codes_bufr_extract_headers_malloc(codes_context* c, const char* filename, codes_bufr_header** result, int* num_messages, int strict_mode); 24 | int codes_extract_offsets_malloc(codes_context* c, const char* filename, ProductKind product, long int** offsets, int* num_messages, int strict_mode); 25 | int codes_extract_offsets_sizes_malloc(codes_context* c, const char* filename, ProductKind product, long int** offsets, size_t** sizes, int* num_messages, int strict_mode); 26 | int codes_bufr_key_is_header(const codes_handle* h, const char* key, int* err); 27 | int codes_bufr_key_is_coordinate(const codes_handle* h, const char* key, int* err); 28 | 29 | char* codes_samples_path(const codes_context *c); 30 | char* codes_definition_path(const codes_context *c); 31 | -------------------------------------------------------------------------------- /gribapi/errors.py: -------------------------------------------------------------------------------- 1 | # 2 | # (C) Copyright 2017- ECMWF. 3 | # 4 | # This software is licensed under the terms of the Apache Licence Version 2.0 5 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # In applying this licence, ECMWF does not waive the privileges and immunities 8 | # granted to it by virtue of its status as an intergovernmental organisation nor 9 | # does it submit to any jurisdiction. 10 | # 11 | 12 | """ 13 | Exception class hierarchy 14 | """ 15 | 16 | from .bindings import ENC, ffi, lib 17 | 18 | 19 | class GribInternalError(Exception): 20 | """ 21 | @brief Wrap errors coming from the C API in a Python exception object. 22 | 23 | Base class for all exceptions 24 | """ 25 | 26 | def __init__(self, value): 27 | # Call the base class constructor with the parameters it needs 28 | Exception.__init__(self, value) 29 | if isinstance(value, int): 30 | self.msg = ffi.string(lib.grib_get_error_message(value)).decode(ENC) 31 | else: 32 | self.msg = value 33 | 34 | def __str__(self): 35 | return self.msg 36 | 37 | 38 | class FunctionalityNotEnabledError(GribInternalError): 39 | """Functionality not enabled.""" 40 | 41 | 42 | class WrongBitmapSizeError(GribInternalError): 43 | """Size of bitmap is incorrect.""" 44 | 45 | 46 | class OutOfRangeError(GribInternalError): 47 | """Value out of coding range.""" 48 | 49 | 50 | class UnsupportedEditionError(GribInternalError): 51 | """Edition not supported..""" 52 | 53 | 54 | class AttributeNotFoundError(GribInternalError): 55 | """Attribute not found..""" 56 | 57 | 58 | class TooManyAttributesError(GribInternalError): 59 | """Too many attributes. Increase MAX_ACCESSOR_ATTRIBUTES.""" 60 | 61 | 62 | class AttributeClashError(GribInternalError): 63 | """Attribute is already present, cannot add.""" 64 | 65 | 66 | class NullPointerError(GribInternalError): 67 | """Null pointer.""" 68 | 69 | 70 | class MissingBufrEntryError(GribInternalError): 71 | """Missing BUFR table entry for descriptor.""" 72 | 73 | 74 | class WrongConversionError(GribInternalError): 75 | """Wrong type conversion.""" 76 | 77 | 78 | class StringTooSmallError(GribInternalError): 79 | """String is smaller than requested.""" 80 | 81 | 82 | class InvalidKeyValueError(GribInternalError): 83 | """Invalid key value.""" 84 | 85 | 86 | class ValueDifferentError(GribInternalError): 87 | """Value is different.""" 88 | 89 | 90 | class DifferentEditionError(GribInternalError): 91 | """Edition of two messages is different.""" 92 | 93 | 94 | class InvalidBitsPerValueError(GribInternalError): 95 | """Invalid number of bits per value.""" 96 | 97 | 98 | class CorruptedIndexError(GribInternalError): 99 | """Index is corrupted.""" 100 | 101 | 102 | class MessageMalformedError(GribInternalError): 103 | """Message malformed.""" 104 | 105 | 106 | class UnderflowError(GribInternalError): 107 | """Underflow.""" 108 | 109 | 110 | class SwitchNoMatchError(GribInternalError): 111 | """Switch unable to find a matching case.""" 112 | 113 | 114 | class ConstantFieldError(GribInternalError): 115 | """Constant field.""" 116 | 117 | 118 | class MessageTooLargeError(GribInternalError): 119 | """Message is too large for the current architecture.""" 120 | 121 | 122 | class InternalArrayTooSmallError(GribInternalError): 123 | """An internal array is too small.""" 124 | 125 | 126 | class PrematureEndOfFileError(GribInternalError): 127 | """End of resource reached when reading message.""" 128 | 129 | 130 | class NullIndexError(GribInternalError): 131 | """Null index.""" 132 | 133 | 134 | class EndOfIndexError(GribInternalError): 135 | """End of index reached.""" 136 | 137 | 138 | class WrongGridError(GribInternalError): 139 | """Grid description is wrong or inconsistent.""" 140 | 141 | 142 | class NoValuesError(GribInternalError): 143 | """Unable to code a field without values.""" 144 | 145 | 146 | class EndError(GribInternalError): 147 | """End of resource.""" 148 | 149 | 150 | class WrongTypeError(GribInternalError): 151 | """Wrong type while packing.""" 152 | 153 | 154 | class NoDefinitionsError(GribInternalError): 155 | """Definitions files not found.""" 156 | 157 | 158 | class HashArrayNoMatchError(GribInternalError): 159 | """Hash array no match.""" 160 | 161 | 162 | class ConceptNoMatchError(GribInternalError): 163 | """Concept no match.""" 164 | 165 | 166 | class OutOfAreaError(GribInternalError): 167 | """The point is out of the grid area.""" 168 | 169 | 170 | class MissingKeyError(GribInternalError): 171 | """Missing a key from the fieldset.""" 172 | 173 | 174 | class InvalidOrderByError(GribInternalError): 175 | """Invalid order by.""" 176 | 177 | 178 | class InvalidNearestError(GribInternalError): 179 | """Invalid nearest id.""" 180 | 181 | 182 | class InvalidKeysIteratorError(GribInternalError): 183 | """Invalid keys iterator id.""" 184 | 185 | 186 | class InvalidIteratorError(GribInternalError): 187 | """Invalid iterator id.""" 188 | 189 | 190 | class InvalidIndexError(GribInternalError): 191 | """Invalid index id.""" 192 | 193 | 194 | class InvalidGribError(GribInternalError): 195 | """Invalid GRIB id.""" 196 | 197 | 198 | class InvalidFileError(GribInternalError): 199 | """Invalid file id.""" 200 | 201 | 202 | class WrongStepUnitError(GribInternalError): 203 | """Wrong units for step (step must be integer).""" 204 | 205 | 206 | class WrongStepError(GribInternalError): 207 | """Unable to set step.""" 208 | 209 | 210 | class InvalidTypeError(GribInternalError): 211 | """Invalid key type.""" 212 | 213 | 214 | class WrongLengthError(GribInternalError): 215 | """Wrong message length.""" 216 | 217 | 218 | class ValueCannotBeMissingError(GribInternalError): 219 | """Value cannot be missing.""" 220 | 221 | 222 | class InvalidSectionNumberError(GribInternalError): 223 | """Invalid section number.""" 224 | 225 | 226 | class NullHandleError(GribInternalError): 227 | """Null handle.""" 228 | 229 | 230 | class InvalidArgumentError(GribInternalError): 231 | """Invalid argument.""" 232 | 233 | 234 | class ReadOnlyError(GribInternalError): 235 | """Value is read only.""" 236 | 237 | 238 | class MemoryAllocationError(GribInternalError): 239 | """Memory allocation error.""" 240 | 241 | 242 | class GeocalculusError(GribInternalError): 243 | """Problem with calculation of geographic attributes.""" 244 | 245 | 246 | class NoMoreInSetError(GribInternalError): 247 | """Code cannot unpack because of string too small.""" 248 | 249 | 250 | class EncodingError(GribInternalError): 251 | """Encoding invalid.""" 252 | 253 | 254 | class DecodingError(GribInternalError): 255 | """Decoding invalid.""" 256 | 257 | 258 | class MessageInvalidError(GribInternalError): 259 | """Message invalid.""" 260 | 261 | 262 | class IOProblemError(GribInternalError): 263 | """Input output problem.""" 264 | 265 | 266 | class KeyValueNotFoundError(GribInternalError): 267 | """Key/value not found.""" 268 | 269 | 270 | class WrongArraySizeError(GribInternalError): 271 | """Array size mismatch.""" 272 | 273 | 274 | class CodeNotFoundInTableError(GribInternalError): 275 | """Code not found in code table.""" 276 | 277 | 278 | class FileNotFoundError(GribInternalError): 279 | """File not found.""" 280 | 281 | 282 | class ArrayTooSmallError(GribInternalError): 283 | """Passed array is too small.""" 284 | 285 | 286 | class MessageEndNotFoundError(GribInternalError): 287 | """Missing 7777 at end of message.""" 288 | 289 | 290 | class FunctionNotImplementedError(GribInternalError): 291 | """Function not yet implemented.""" 292 | 293 | 294 | class BufferTooSmallError(GribInternalError): 295 | """Passed buffer is too small.""" 296 | 297 | 298 | class InternalError(GribInternalError): 299 | """Internal error.""" 300 | 301 | 302 | class EndOfFileError(GribInternalError): 303 | """End of resource reached.""" 304 | 305 | 306 | ERROR_MAP = { 307 | -67: FunctionalityNotEnabledError, 308 | -66: WrongBitmapSizeError, 309 | -65: OutOfRangeError, 310 | -64: UnsupportedEditionError, 311 | -63: AttributeNotFoundError, 312 | -62: TooManyAttributesError, 313 | -61: AttributeClashError, 314 | -60: NullPointerError, 315 | -59: MissingBufrEntryError, 316 | -58: WrongConversionError, 317 | -57: StringTooSmallError, 318 | -56: InvalidKeyValueError, 319 | -55: ValueDifferentError, 320 | -54: DifferentEditionError, 321 | -53: InvalidBitsPerValueError, 322 | -52: CorruptedIndexError, 323 | -51: MessageMalformedError, 324 | -50: UnderflowError, 325 | -49: SwitchNoMatchError, 326 | -48: ConstantFieldError, 327 | -47: MessageTooLargeError, 328 | -46: InternalArrayTooSmallError, 329 | -45: PrematureEndOfFileError, 330 | -44: NullIndexError, 331 | -43: EndOfIndexError, 332 | -42: WrongGridError, 333 | -41: NoValuesError, 334 | -40: EndError, 335 | -39: WrongTypeError, 336 | -38: NoDefinitionsError, 337 | -37: HashArrayNoMatchError, 338 | -36: ConceptNoMatchError, 339 | -35: OutOfAreaError, 340 | -34: MissingKeyError, 341 | -33: InvalidOrderByError, 342 | -32: InvalidNearestError, 343 | -31: InvalidKeysIteratorError, 344 | -30: InvalidIteratorError, 345 | -29: InvalidIndexError, 346 | -28: InvalidGribError, 347 | -27: InvalidFileError, 348 | -26: WrongStepUnitError, 349 | -25: WrongStepError, 350 | -24: InvalidTypeError, 351 | -23: WrongLengthError, 352 | -22: ValueCannotBeMissingError, 353 | -21: InvalidSectionNumberError, 354 | -20: NullHandleError, 355 | -19: InvalidArgumentError, 356 | -18: ReadOnlyError, 357 | -17: MemoryAllocationError, 358 | -16: GeocalculusError, 359 | -15: NoMoreInSetError, 360 | -14: EncodingError, 361 | -13: DecodingError, 362 | -12: MessageInvalidError, 363 | -11: IOProblemError, 364 | -10: KeyValueNotFoundError, 365 | -9: WrongArraySizeError, 366 | -8: CodeNotFoundInTableError, 367 | -7: FileNotFoundError, 368 | -6: ArrayTooSmallError, 369 | -5: MessageEndNotFoundError, 370 | -4: FunctionNotImplementedError, 371 | -3: BufferTooSmallError, 372 | -2: InternalError, 373 | -1: EndOfFileError, 374 | } 375 | 376 | 377 | def raise_grib_error(errid): 378 | """ 379 | Raise the GribInternalError corresponding to ``errid``. 380 | """ 381 | raise ERROR_MAP[errid](errid) 382 | -------------------------------------------------------------------------------- /gribapi/grib_api.h: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2017- ECMWF. 3 | * 4 | * This software is licensed under the terms of the Apache Licence Version 2.0 5 | * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | * 7 | * In applying this licence, ECMWF does not waive the privileges and immunities granted to it by 8 | * virtue of its status as an intergovernmental organisation nor does it submit to any jurisdiction. 9 | */ 10 | 11 | typedef enum ProductKind {PRODUCT_ANY, PRODUCT_GRIB, PRODUCT_BUFR, PRODUCT_METAR, PRODUCT_GTS, PRODUCT_TAF} ProductKind; 12 | 13 | #define GRIB_TYPE_UNDEFINED 0 14 | #define GRIB_TYPE_LONG 1 15 | #define GRIB_TYPE_DOUBLE 2 16 | #define GRIB_TYPE_STRING 3 17 | #define GRIB_TYPE_BYTES 4 18 | #define GRIB_TYPE_SECTION 5 19 | #define GRIB_TYPE_LABEL 6 20 | #define GRIB_TYPE_MISSING 7 21 | 22 | #define GRIB_KEYS_ITERATOR_SKIP_READ_ONLY 1 23 | #define GRIB_KEYS_ITERATOR_SKIP_EDITION_SPECIFIC 4 24 | #define GRIB_KEYS_ITERATOR_SKIP_CODED 8 25 | #define GRIB_KEYS_ITERATOR_SKIP_COMPUTED 16 26 | #define GRIB_KEYS_ITERATOR_SKIP_DUPLICATES 32 27 | #define GRIB_KEYS_ITERATOR_SKIP_FUNCTION 64 28 | 29 | typedef struct grib_values grib_values; 30 | 31 | struct grib_values { 32 | const char* name; 33 | int type; 34 | long long_value; 35 | double double_value; 36 | const char* string_value; 37 | int error; 38 | int has_value; 39 | int equal; 40 | grib_values* next; 41 | }; 42 | 43 | typedef struct grib_handle grib_handle; 44 | typedef struct grib_multi_handle grib_multi_handle; 45 | typedef struct grib_context grib_context; 46 | typedef struct grib_iterator grib_iterator; 47 | typedef struct grib_nearest grib_nearest; 48 | typedef struct grib_keys_iterator grib_keys_iterator; 49 | typedef struct bufr_keys_iterator bufr_keys_iterator; 50 | typedef struct grib_index grib_index; 51 | 52 | grib_index* grib_index_new_from_file(grib_context* c, 53 | char* filename,const char* keys,int *err); 54 | 55 | int grib_index_add_file(grib_index *index, const char *filename); 56 | int grib_index_write(grib_index *index, const char *filename); 57 | grib_index* grib_index_read(grib_context* c,const char* filename,int *err); 58 | 59 | int grib_index_get_size(const grib_index* index,const char* key,size_t* size); 60 | int grib_index_get_long(const grib_index* index,const char* key, 61 | long* values,size_t *size); 62 | int grib_index_get_double(const grib_index* index, const char* key, 63 | double* values, size_t* size); 64 | int grib_index_get_string(const grib_index* index,const char* key, 65 | char** values,size_t *size); 66 | int grib_index_select_long(grib_index* index,const char* key,long value); 67 | int grib_index_select_double(grib_index* index,const char* key,double value); 68 | int grib_index_select_string(grib_index* index,const char* key,char* value); 69 | grib_handle* grib_handle_new_from_index(grib_index* index,int *err); 70 | 71 | void grib_index_delete(grib_index* index); 72 | 73 | int grib_count_in_file(grib_context* c, FILE* f,int* n); 74 | grib_handle* grib_handle_new_from_file(grib_context* c, FILE* f, int* error); 75 | grib_handle* grib_handle_new_from_message_copy(grib_context* c, const void* data, size_t data_len); 76 | grib_handle* grib_handle_new_from_samples (grib_context* c, const char* sample_name); 77 | grib_handle* grib_handle_clone(const grib_handle* h); 78 | grib_handle* grib_handle_clone_headers_only(const grib_handle* h); 79 | int grib_handle_delete(grib_handle* h); 80 | grib_multi_handle* grib_multi_handle_new(grib_context* c); 81 | int grib_multi_handle_append(grib_handle* h,int start_section,grib_multi_handle* mh); 82 | int grib_multi_handle_delete(grib_multi_handle* mh); 83 | int grib_multi_handle_write(grib_multi_handle* mh,FILE* f); 84 | 85 | int grib_get_message(const grib_handle* h ,const void** message, size_t *message_length); 86 | 87 | grib_iterator* grib_iterator_new(const grib_handle* h, unsigned long flags, int* error); 88 | int grib_iterator_next(grib_iterator *i, double* lat,double* lon,double* value); 89 | int grib_iterator_delete(grib_iterator *i); 90 | grib_nearest* grib_nearest_new(const grib_handle* h, int* error); 91 | 92 | int grib_nearest_find(grib_nearest *nearest, const grib_handle* h, double inlat, double inlon, 93 | unsigned long flags,double* outlats,double* outlons, 94 | double* values,double* distances,int* indexes,size_t *len); 95 | 96 | int grib_nearest_delete(grib_nearest *nearest); 97 | 98 | int grib_nearest_find_multiple(const grib_handle* h, int is_lsm, 99 | const double* inlats, const double* inlons, long npoints, 100 | double* outlats, double* outlons, 101 | double* values, double* distances, int* indexes); 102 | 103 | int grib_get_offset(const grib_handle* h, const char* key, size_t* offset); 104 | int grib_get_size(const grib_handle* h, const char* key,size_t *size); 105 | 106 | int grib_get_length(const grib_handle* h, const char* key,size_t *length); 107 | int grib_get_long(const grib_handle* h, const char* key, long* value); 108 | int grib_get_double(const grib_handle* h, const char* key, double* value); 109 | int grib_get_double_element(const grib_handle* h, const char* key, int i, double* value); 110 | int grib_get_double_elements(const grib_handle* h, const char* key, const int* index_array, long size, double* value); 111 | int grib_get_string(const grib_handle* h, const char* key, char* value, size_t *length); 112 | int grib_get_string_array(const grib_handle* h, const char* key, char** vals, size_t *length); 113 | int grib_get_double_array(const grib_handle* h, const char* key, double* vals, size_t *length); 114 | int grib_get_float_array(const grib_handle* h, const char* key, float* vals, size_t *length); 115 | int grib_get_long_array(const grib_handle* h, const char* key, long* vals, size_t *length); 116 | 117 | int grib_copy_namespace(grib_handle* dest, const char* name, grib_handle* src); 118 | int grib_set_long(grib_handle* h, const char* key, long val); 119 | int grib_set_double(grib_handle* h, const char* key, double val); 120 | int grib_set_string(grib_handle* h, const char* key, const char* mesg, size_t *length); 121 | int grib_set_double_array(grib_handle* h, const char* key , const double* vals , size_t length); 122 | int grib_set_long_array(grib_handle* h, const char* key , const long* vals, size_t length); 123 | 124 | int grib_set_string_array(grib_handle* h, const char *key, const char **vals, size_t length); 125 | 126 | void grib_dump_content(const grib_handle* h, FILE* out, const char* mode, unsigned long option_flags, void* arg); 127 | grib_context* grib_context_get_default(void); 128 | void grib_context_delete(grib_context* c); 129 | 130 | void grib_gts_header_on(grib_context* c); 131 | void grib_gts_header_off(grib_context* c); 132 | void grib_gribex_mode_on(grib_context* c); 133 | void grib_gribex_mode_off(grib_context* c); 134 | void grib_context_set_definitions_path(grib_context* c, const char* path); 135 | void grib_context_set_debug(grib_context* c, int mode); 136 | void grib_context_set_data_quality_checks(grib_context* c, int val); 137 | void grib_context_set_samples_path(grib_context* c, const char* path); 138 | void grib_multi_support_on(grib_context* c); 139 | void grib_multi_support_off(grib_context* c); 140 | void grib_multi_support_reset_file(grib_context* c, FILE* f); 141 | long grib_get_api_version(void); 142 | 143 | char* grib_samples_path(const grib_context *c); 144 | char* grib_definition_path(const grib_context *c); 145 | 146 | grib_keys_iterator* grib_keys_iterator_new(grib_handle* h,unsigned long filter_flags, const char* name_space); 147 | bufr_keys_iterator* codes_bufr_keys_iterator_new(grib_handle* h, unsigned long filter_flags); 148 | 149 | int grib_keys_iterator_next(grib_keys_iterator* kiter); 150 | int codes_bufr_keys_iterator_next(bufr_keys_iterator* kiter); 151 | 152 | const char* grib_keys_iterator_get_name(const grib_keys_iterator *kiter); 153 | char* codes_bufr_keys_iterator_get_name(const bufr_keys_iterator* kiter); 154 | 155 | int grib_keys_iterator_delete(grib_keys_iterator* kiter); 156 | int codes_bufr_keys_iterator_delete(bufr_keys_iterator* kiter); 157 | 158 | int grib_keys_iterator_rewind(grib_keys_iterator* kiter); 159 | int codes_bufr_keys_iterator_rewind(bufr_keys_iterator* kiter); 160 | 161 | int grib_keys_iterator_set_flags(grib_keys_iterator *kiter,unsigned long flags); 162 | const char* grib_get_error_message(int code); 163 | 164 | int grib_get_native_type(const grib_handle* h, const char* name,int* type); 165 | 166 | /* aa: changed off_t to long int */ 167 | int grib_get_message_offset(const grib_handle* h,long int* offset); 168 | 169 | int grib_set_values(grib_handle* h,grib_values* grib_values , size_t arg_count); 170 | int grib_is_missing(const grib_handle* h, const char* key, int* err); 171 | int grib_is_defined(const grib_handle* h, const char* key); 172 | int grib_set_missing(grib_handle* h, const char* key); 173 | 174 | int grib_get_message_size(const grib_handle* h,size_t* size); 175 | int parse_keyval_string(const char *grib_tool, char *arg, int values_required, int default_type, grib_values values[], int *count); 176 | 177 | int grib_get_data(const grib_handle *h, double *lats, double *lons, double *values); 178 | int grib_get_gaussian_latitudes(long trunc, double* lats); 179 | 180 | int codes_is_feature_enabled(const char* feature); 181 | int codes_get_features(char* result, size_t* length, int select); 182 | 183 | /* EXPERIMENTAL */ 184 | typedef struct codes_bufr_header { 185 | unsigned long message_offset; 186 | unsigned long message_size; 187 | 188 | /* Section 0 keys */ 189 | long edition; 190 | 191 | /* Section 1 keys */ 192 | long masterTableNumber; 193 | long bufrHeaderSubCentre; 194 | long bufrHeaderCentre; 195 | long updateSequenceNumber; 196 | long dataCategory; 197 | long dataSubCategory; 198 | long masterTablesVersionNumber; 199 | long localTablesVersionNumber; 200 | 201 | long typicalYear; 202 | long typicalMonth; 203 | long typicalDay; 204 | long typicalHour; 205 | long typicalMinute; 206 | long typicalSecond; 207 | long typicalDate; 208 | long typicalTime; 209 | 210 | long internationalDataSubCategory; 211 | 212 | long localSectionPresent; 213 | long ecmwfLocalSectionPresent; 214 | 215 | /* ECMWF local section keys */ 216 | long rdbType; 217 | long oldSubtype; 218 | long rdbSubtype; 219 | char ident[9]; 220 | long localYear; 221 | long localMonth; 222 | long localDay; 223 | long localHour; 224 | long localMinute; 225 | long localSecond; 226 | 227 | long rdbtimeDay; 228 | long rdbtimeHour; 229 | long rdbtimeMinute; 230 | long rdbtimeSecond; 231 | 232 | long rectimeDay; 233 | long rectimeHour; 234 | long rectimeMinute; 235 | long rectimeSecond; 236 | long restricted; 237 | 238 | long isSatellite; 239 | double localLongitude1; 240 | double localLatitude1; 241 | double localLongitude2; 242 | double localLatitude2; 243 | double localLatitude; 244 | double localLongitude; 245 | long localNumberOfObservations; 246 | long satelliteID; 247 | long qualityControl; 248 | long newSubtype; 249 | long daLoop; 250 | 251 | /* Section 3 keys */ 252 | unsigned long numberOfSubsets; 253 | long observedData; 254 | long compressedData; 255 | 256 | } codes_bufr_header; 257 | 258 | 259 | /** No error */ 260 | #define GRIB_SUCCESS 0 261 | /** End of resource reached */ 262 | #define GRIB_END_OF_FILE -1 263 | /** Internal error */ 264 | #define GRIB_INTERNAL_ERROR -2 265 | /** Passed buffer is too small */ 266 | #define GRIB_BUFFER_TOO_SMALL -3 267 | /** Function not yet implemented */ 268 | #define GRIB_NOT_IMPLEMENTED -4 269 | /** Missing 7777 at end of message */ 270 | #define GRIB_7777_NOT_FOUND -5 271 | /** Passed array is too small */ 272 | #define GRIB_ARRAY_TOO_SMALL -6 273 | /** File not found */ 274 | #define GRIB_FILE_NOT_FOUND -7 275 | /** Code not found in code table */ 276 | #define GRIB_CODE_NOT_FOUND_IN_TABLE -8 277 | /** Array size mismatch */ 278 | #define GRIB_WRONG_ARRAY_SIZE -9 279 | /** Key/value not found */ 280 | #define GRIB_NOT_FOUND -10 281 | /** Input output problem */ 282 | #define GRIB_IO_PROBLEM -11 283 | /** Message invalid */ 284 | #define GRIB_INVALID_MESSAGE -12 285 | /** Decoding invalid */ 286 | #define GRIB_DECODING_ERROR -13 287 | /** Encoding invalid */ 288 | #define GRIB_ENCODING_ERROR -14 289 | /** Code cannot unpack because of string too small */ 290 | #define GRIB_NO_MORE_IN_SET -15 291 | /** Problem with calculation of geographic attributes */ 292 | #define GRIB_GEOCALCULUS_PROBLEM -16 293 | /** Memory allocation error */ 294 | #define GRIB_OUT_OF_MEMORY -17 295 | /** Value is read only */ 296 | #define GRIB_READ_ONLY -18 297 | /** Invalid argument */ 298 | #define GRIB_INVALID_ARGUMENT -19 299 | /** Null handle */ 300 | #define GRIB_NULL_HANDLE -20 301 | /** Invalid section number */ 302 | #define GRIB_INVALID_SECTION_NUMBER -21 303 | /** Value cannot be missing */ 304 | #define GRIB_VALUE_CANNOT_BE_MISSING -22 305 | /** Wrong message length */ 306 | #define GRIB_WRONG_LENGTH -23 307 | /** Invalid key type */ 308 | #define GRIB_INVALID_TYPE -24 309 | /** Unable to set step */ 310 | #define GRIB_WRONG_STEP -25 311 | /** Wrong units for step (step must be integer) */ 312 | #define GRIB_WRONG_STEP_UNIT -26 313 | /** Invalid file id */ 314 | #define GRIB_INVALID_FILE -27 315 | /** Invalid grib id */ 316 | #define GRIB_INVALID_GRIB -28 317 | /** Invalid index id */ 318 | #define GRIB_INVALID_INDEX -29 319 | /** Invalid iterator id */ 320 | #define GRIB_INVALID_ITERATOR -30 321 | /** Invalid keys iterator id */ 322 | #define GRIB_INVALID_KEYS_ITERATOR -31 323 | /** Invalid nearest id */ 324 | #define GRIB_INVALID_NEAREST -32 325 | /** Invalid order by */ 326 | #define GRIB_INVALID_ORDERBY -33 327 | /** Missing a key from the fieldset */ 328 | #define GRIB_MISSING_KEY -34 329 | /** The point is out of the grid area */ 330 | #define GRIB_OUT_OF_AREA -35 331 | /** Concept no match */ 332 | #define GRIB_CONCEPT_NO_MATCH -36 333 | /** Hash array no match */ 334 | #define GRIB_HASH_ARRAY_NO_MATCH -37 335 | /** Definitions files not found */ 336 | #define GRIB_NO_DEFINITIONS -38 337 | /** Wrong type while packing */ 338 | #define GRIB_WRONG_TYPE -39 339 | /** End of resource */ 340 | #define GRIB_END -40 341 | /** Unable to code a field without values */ 342 | #define GRIB_NO_VALUES -41 343 | /** Grid description is wrong or inconsistent */ 344 | #define GRIB_WRONG_GRID -42 345 | /** End of index reached */ 346 | #define GRIB_END_OF_INDEX -43 347 | /** Null index */ 348 | #define GRIB_NULL_INDEX -44 349 | /** End of resource reached when reading message */ 350 | #define GRIB_PREMATURE_END_OF_FILE -45 351 | /** An internal array is too small */ 352 | #define GRIB_INTERNAL_ARRAY_TOO_SMALL -46 353 | /** Message is too large for the current architecture */ 354 | #define GRIB_MESSAGE_TOO_LARGE -47 355 | /** Constant field */ 356 | #define GRIB_CONSTANT_FIELD -48 357 | /** Switch unable to find a matching case */ 358 | #define GRIB_SWITCH_NO_MATCH -49 359 | /** Underflow */ 360 | #define GRIB_UNDERFLOW -50 361 | /** Message malformed */ 362 | #define GRIB_MESSAGE_MALFORMED -51 363 | /** Index is corrupted */ 364 | #define GRIB_CORRUPTED_INDEX -52 365 | /** Invalid number of bits per value */ 366 | #define GRIB_INVALID_BPV -53 367 | /** Edition of two messages is different */ 368 | #define GRIB_DIFFERENT_EDITION -54 369 | /** Value is different */ 370 | #define GRIB_VALUE_DIFFERENT -55 371 | /** Invalid key value */ 372 | #define GRIB_INVALID_KEY_VALUE -56 373 | /** String is smaller than requested */ 374 | #define GRIB_STRING_TOO_SMALL -57 375 | /** Wrong type conversion */ 376 | #define GRIB_WRONG_CONVERSION -58 377 | /** Missing BUFR table entry for descriptor */ 378 | #define GRIB_MISSING_BUFR_ENTRY -59 379 | /** Null pointer */ 380 | #define GRIB_NULL_POINTER -60 381 | /** Attribute is already present, cannot add */ 382 | #define GRIB_ATTRIBUTE_CLASH -61 383 | /** Too many attributes. Increase MAX_ACCESSOR_ATTRIBUTES */ 384 | #define GRIB_TOO_MANY_ATTRIBUTES -62 385 | /** Attribute not found. */ 386 | #define GRIB_ATTRIBUTE_NOT_FOUND -63 387 | /** Edition not supported. */ 388 | #define GRIB_UNSUPPORTED_EDITION -64 389 | /** Value out of coding range */ 390 | #define GRIB_OUT_OF_RANGE -65 391 | /** Size of bitmap is incorrect */ 392 | #define GRIB_WRONG_BITMAP_SIZE -66 393 | /** Functionality not enabled */ 394 | #define GRIB_FUNCTIONALITY_NOT_ENABLED -67 395 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | ; addopts=-s --cov climetlab --verbose --cov-report xml --cov-report html 3 | ; addopts=--no-cov 4 | addopts=-s --verbose 5 | -------------------------------------------------------------------------------- /scripts/build-linux.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # (C) Copyright 2024- ECMWF. 3 | # 4 | # This software is licensed under the terms of the Apache Licence Version 2.0 5 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | # In applying this licence, ECMWF does not waive the privileges and immunities 7 | # granted to it by virtue of its status as an intergovernmental organisation 8 | # nor does it submit to any jurisdiction. 9 | # (rm -fr build-other/netcdf/; cd src/netcdf/; git checkout -- .; git clean -f .) 10 | set -eaux 11 | 12 | # ensure the cleanup task can delete our workspace 13 | umask 0000 14 | chmod -R a+w . 15 | 16 | pwd 17 | 18 | GIT_OPENJPEG=https://github.com/uclouvain/openjpeg 19 | OPENJPEG_VERSION=v2.5.2 20 | 21 | # To allow the manylinux image to continue to use yum afer EOL. See, for example: 22 | # https://github.com/zanmato1984/arrow/commit/1fe15e06fac23983e5f890c2d749d9ccecd2ca15 23 | # https://github.com/apache/arrow/issues/43119 24 | #sudo sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/*.repo 25 | #sudo sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/*.repo 26 | #sudo sed -i s/^mirrorlist=http/#mirrorlist=http/g /etc/yum.repos.d/*.repo 27 | 28 | source scripts/common.sh 29 | 30 | for p in libaec-devel libpng-devel gobject-introspection-devel 31 | do 32 | sudo yum install -y $p 33 | # There may be a better way 34 | sudo yum install $p 2>&1 > tmp 35 | cat tmp 36 | v=$(grep 'already installed' < tmp | awk '{print $2;}' | sed 's/\\d://') 37 | echo "yum $p $v" >> versions 38 | done 39 | 40 | 41 | sudo yum install -y flex bison 42 | 43 | sudo ln -sf /opt/python/cp310-cp310/bin/python /usr/local/bin/python3 44 | sudo ln -sf /opt/python/cp310-cp310/bin/python3-config /usr/local/bin/python3-config 45 | sudo ln -sf /opt/python/cp310-cp310/bin/pip /usr/local/bin/pip3 46 | 47 | sudo pip3 install ninja auditwheel meson 'setuptools>=72.1.0' 48 | 49 | sudo ln -sf /opt/python/cp310-cp310/bin/meson /usr/local/bin/meson 50 | sudo ln -sf /opt/python/cp310-cp310/bin/ninja /usr/local/bin/ninja 51 | 52 | PKG_CONFIG_PATH=/usr/lib64/pkgconfig:/usr/lib/pkgconfig:$PKG_CONFIG_PATH 53 | PKG_CONFIG_PATH=$TOPDIR/install/lib/pkgconfig:$TOPDIR/install/lib64/pkgconfig:$PKG_CONFIG_PATH 54 | LD_LIBRARY_PATH=$TOPDIR/install/lib:$TOPDIR/install/lib64:$LD_LIBRARY_PATH 55 | 56 | # Build openjpeg 57 | # - because the one supplied with 'yum' is built with fast-math, which interferes with Python 58 | 59 | [[ -d src/openjpeg ]] || git clone --branch $OPENJPEG_VERSION --depth=1 $GIT_OPENJPEG src/openjpeg 60 | 61 | mkdir -p $TOPDIR/build-binaries/openjpeg 62 | cd $TOPDIR/build-binaries/openjpeg 63 | 64 | cmake \ 65 | $TOPDIR/src/openjpeg/ \ 66 | -DCMAKE_BUILD_TYPE=RelWithDebInfo \ 67 | -DCMAKE_INSTALL_PREFIX=$TOPDIR/install 68 | 69 | cd $TOPDIR 70 | cmake --build build-binaries/openjpeg --target install 71 | 72 | # Build eccodes 73 | 74 | cd $TOPDIR/build-binaries/eccodes 75 | 76 | $TOPDIR/src/ecbuild/bin/ecbuild \ 77 | $TOPDIR/src/eccodes \ 78 | -GNinja \ 79 | -DCMAKE_BUILD_TYPE=RelWithDebInfo \ 80 | -DENABLE_PYTHON=0 \ 81 | -DENABLE_BUILD_TOOLS=0 \ 82 | -DENABLE_ECCODES_THREADS=1 \ 83 | -DENABLE_JPG=1 \ 84 | -DENABLE_JPG_LIBJASPER=0 \ 85 | -DENABLE_JPG_LIBOPENJPEG=1 \ 86 | -DOPENJPEG_DIR=$TOPDIR/install \ 87 | -DENABLE_MEMFS=1 \ 88 | -DENABLE_INSTALL_ECCODES_DEFINITIONS=0 \ 89 | -DENABLE_INSTALL_ECCODES_SAMPLES=0 \ 90 | -DCMAKE_INSTALL_PREFIX=$TOPDIR/install $ECCODES_COMMON_CMAKE_OPTIONS 91 | 92 | cd $TOPDIR 93 | cmake --build build-binaries/eccodes --target install 94 | 95 | 96 | 97 | # Create wheel 98 | 99 | mkdir -p install/lib/ 100 | cp install/lib64/*.so install/lib/ 101 | strip --strip-debug install/lib/*.so 102 | 103 | ./scripts/versions.sh > eccodes/versions.txt 104 | -------------------------------------------------------------------------------- /scripts/build-macos.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # (C) Copyright 2024- ECMWF. 3 | # 4 | # This software is licensed under the terms of the Apache Licence Version 2.0 5 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | # In applying this licence, ECMWF does not waive the privileges and immunities 7 | # granted to it by virtue of its status as an intergovernmental organisation 8 | # nor does it submit to any jurisdiction. 9 | 10 | set -eaux 11 | python_version=$1 12 | 13 | uname -a 14 | 15 | # HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 16 | HOMEBREW_NO_INSTALL_CLEANUP=1 17 | 18 | arch=$(arch) 19 | [[ $arch == "i386" ]] && arch="x86_64" # GitHub Actions on macOS declare i386 20 | 21 | ARCH="arch -$arch" 22 | 23 | source scripts/common.sh 24 | source scripts/select-python-macos.sh $python_version 25 | 26 | 27 | #$ARCH brew install cmake ninja pkg-config automake 28 | #$ARCH brew install cmake ninja netcdf libaec 29 | 30 | 31 | for p in netcdf 32 | do 33 | v=$(brew info $p | grep Cellar | awk '{print $1;}' | awk -F/ '{print $NF;}') 34 | echo "brew $p $v" >> versions 35 | done 36 | 37 | # Build eccodes 38 | 39 | cd $TOPDIR/build-binaries/eccodes 40 | 41 | # We disable JASPER because of a linking issue. JPEG support comes from 42 | # other libraries (e.g. openjpeg) 43 | $ARCH $TOPDIR/src/ecbuild/bin/ecbuild \ 44 | $TOPDIR/src/eccodes \ 45 | -GNinja \ 46 | -DCMAKE_OSX_ARCHITECTURES=$arch \ 47 | -DCMAKE_BUILD_TYPE=RelWithDebInfo \ 48 | -DENABLE_FORTRAN=0 \ 49 | -DENABLE_BUILD_TOOLS=0 \ 50 | -DENABLE_ECCODES_THREADS=1 \ 51 | -DENABLE_JPG_LIBJASPER=0 \ 52 | -DENABLE_MEMFS=1 \ 53 | -DENABLE_INSTALL_ECCODES_DEFINITIONS=0 \ 54 | -DENABLE_INSTALL_ECCODES_SAMPLES=0 \ 55 | -DCMAKE_INSTALL_PREFIX=$TOPDIR/install \ 56 | -DCMAKE_INSTALL_RPATH=$TOPDIR/install/lib $ECCODES_COMMON_CMAKE_OPTIONS 57 | 58 | cd $TOPDIR 59 | $ARCH cmake --build build-binaries/eccodes --target install 60 | 61 | # Run some basic tests to check the library is ok 62 | cd build-binaries/eccodes 63 | ctest -L sanity 64 | cd $TOPDIR 65 | 66 | # Create wheel 67 | rm -fr dist wheelhouse 68 | 69 | 70 | # echo "================================================================================" 71 | # for n in install/lib/*.dylib 72 | # do 73 | # echo $n 74 | # ./scripts/libs-macos.py $n 75 | # done 76 | # echo "================================================================================" 77 | 78 | strip -S install/lib/*.dylib 79 | 80 | ./scripts/versions.sh > gribapi/binary-versions.txt 81 | -------------------------------------------------------------------------------- /scripts/build-windows.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env bash 3 | # (C) Copyright 2024- ECMWF. 4 | # 5 | # This software is licensed under the terms of the Apache Licence Version 2.0 6 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 7 | # In applying this licence, ECMWF does not waive the privileges and immunities 8 | # granted to it by virtue of its status as an intergovernmental organisation 9 | # nor does it submit to any jurisdiction. 10 | 11 | set -eaux 12 | 13 | source scripts/common.sh 14 | 15 | here=$(pwd) 16 | cd $VCPKG_INSTALLATION_ROOT 17 | url=$(git remote -v | head -1 | awk '{print $2;}') 18 | sha1=$(git rev-parse HEAD) 19 | cd $here 20 | 21 | echo git $url $sha1 > versions 22 | 23 | # the results of the following suggested that pkg-config.exe is not installed on the latest 24 | # Windows runner 25 | #find /c/ -name pkg-config.exe 26 | #exit 1 27 | 28 | # if [[ $WINARCH == "x64" ]]; then 29 | # PKG_CONFIG_EXECUTABLE=/c/rtools43/mingw64/bin/pkg-config.exe 30 | # else 31 | # PKG_CONFIG_EXECUTABLE=/c/rtools43/mingw32/bin/pkg-config.exe 32 | # fi 33 | 34 | vcpkg install pkgconf 35 | 36 | for p in libpng 37 | do 38 | vcpkg install $p:$WINARCH-windows 39 | n=$(echo $p | sed 's/\[.*//') 40 | v=$(vcpkg list $n | awk '{print $2;}') 41 | echo "vcpkg $n $v" >> versions 42 | done 43 | 44 | echo ================================================================= 45 | find $VCPKG_INSTALLATION_ROOT -type f -name png.h -print 46 | echo ================================================================= 47 | 48 | 49 | pip install ninja wheel dll-diagnostics 50 | 51 | echo "pip $(pip freeze | grep dll-diagnostics | sed 's/==/ /')" >> versions 52 | 53 | # Build libaec 54 | git clone $GIT_AEC src/aec 55 | cd src/aec 56 | git checkout $AEC_VERSION 57 | cd $TOPDIR 58 | mkdir -p build-binaries/aec 59 | cd build-binaries/aec 60 | 61 | cmake \ 62 | $TOPDIR/src/aec -G"NMake Makefiles" \ 63 | -DCMAKE_BUILD_TYPE=Release \ 64 | -DCMAKE_INSTALL_PREFIX=$TOPDIR/install \ 65 | -DCMAKE_TOOLCHAIN_FILE=/c/vcpkg/scripts/buildsystems/vcpkg.cmake \ 66 | -DCMAKE_C_COMPILER=cl.exe 67 | 68 | cd $TOPDIR 69 | cmake --build build-binaries/aec --target install 70 | 71 | 72 | 73 | # Build eccodes 74 | 75 | cd $TOPDIR/build-binaries/eccodes 76 | 77 | $TOPDIR/src/ecbuild/bin/ecbuild \ 78 | $TOPDIR/src/eccodes \ 79 | -G"NMake Makefiles" \ 80 | -DCMAKE_BUILD_TYPE=RelWithDebInfo \ 81 | -DENABLE_PYTHON=0 \ 82 | -DENABLE_FORTRAN=0 \ 83 | -DENABLE_BUILD_TOOLS=0 \ 84 | -DENABLE_MEMFS=1 \ 85 | -DENABLE_INSTALL_ECCODES_DEFINITIONS=0 \ 86 | -DENABLE_INSTALL_ECCODES_SAMPLES=0 \ 87 | -DCMAKE_INSTALL_PREFIX=$TOPDIR/install \ 88 | -DCMAKE_TOOLCHAIN_FILE=/c/vcpkg/scripts/buildsystems/vcpkg.cmake \ 89 | -DCMAKE_C_COMPILER=cl.exe $ECCODES_COMMON_CMAKE_OPTIONS 90 | 91 | # -DPKG_CONFIG_EXECUTABLE=$PKG_CONFIG_EXECUTABLE 92 | 93 | cd $TOPDIR 94 | cmake --build build-binaries/eccodes --target install 95 | 96 | 97 | # Create wheel 98 | 99 | rm -fr dist wheelhouse eccodes/share 100 | python scripts/copy-dlls.py install/bin/eccodes.dll eccodes/ 101 | 102 | pip install -r scripts/requirements.txt 103 | find eccodes -name '*.dll' > libs 104 | cat libs 105 | python ./scripts/copy-licences.py libs 106 | 107 | mkdir -p install/include 108 | 109 | ./scripts/versions.sh > eccodes/versions.txt 110 | -------------------------------------------------------------------------------- /scripts/common.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # (C) Copyright 2024- ECMWF. 3 | # 4 | # This software is licensed under the terms of the Apache Licence Version 2.0 5 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | # In applying this licence, ECMWF does not waive the privileges and immunities 7 | # granted to it by virtue of its status as an intergovernmental organisation 8 | # nor does it submit to any jurisdiction. 9 | 10 | set -eaux 11 | rm -f versions 12 | 13 | GIT_ECBUILD=https://github.com/ecmwf/ecbuild.git 14 | ECBUILD_VERSION=master 15 | 16 | GIT_ECCODES=https://github.com/ecmwf/eccodes.git 17 | ECCODES_VERSION=2.42.0 18 | ECCODES_COMMON_CMAKE_OPTIONS="-DENABLE_PNG=1 -DENABLE_JPG=1 -DENABLE_NETCDF=0 -DENABLE_EXAMPLES=0" 19 | 20 | GIT_AEC=https://github.com/MathisRosenhauer/libaec.git 21 | AEC_VERSION=master 22 | 23 | rm -fr src build build-binaries 24 | 25 | git clone --branch $ECBUILD_VERSION --depth=1 $GIT_ECBUILD src/ecbuild 26 | git clone --branch $ECCODES_VERSION --depth=1 $GIT_ECCODES src/eccodes 27 | 28 | mkdir -p build-binaries/eccodes 29 | 30 | TOPDIR=$(/bin/pwd) 31 | 32 | echo "================================================================================" 33 | env | sort 34 | echo "================================================================================" 35 | -------------------------------------------------------------------------------- /scripts/copy-dlls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # (C) Copyright 2024- ECMWF. 4 | # 5 | # This software is licensed under the terms of the Apache Licence Version 2.0 6 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 7 | # 8 | # In applying this licence, ECMWF does not waive the privileges and immunities 9 | # granted to it by virtue of its status as an intergovernmental organisation nor 10 | # does it submit to any jurisdiction. 11 | # 12 | 13 | import os 14 | import shutil 15 | import sys 16 | 17 | from dlldiag.common import ModuleHeader 18 | 19 | VCPKG1 = "C:/vcpkg/installed/{}-windows/bin/{}" 20 | VCPKG2 = "C:/vcpkg/installed/{}-windows/debug/bin/{}" 21 | 22 | 23 | def scan_module(module, depth, seen): 24 | name = os.path.basename(module) 25 | 26 | if name in seen: 27 | return 28 | 29 | if not os.path.exists(module): 30 | return 31 | 32 | print(" " * depth, module) 33 | seen[name] = module 34 | 35 | header = ModuleHeader(module) 36 | cwd = os.path.dirname(module) 37 | architecture = header.getArchitecture() 38 | for dll in header.listAllImports(): 39 | # print("DEBUG", dll) 40 | scan_module((cwd + "/" + dll), depth + 3, seen) 41 | scan_module(VCPKG1.format(architecture, dll), depth + 3, seen) 42 | scan_module(VCPKG2.format(architecture, dll), depth + 3, seen) 43 | 44 | 45 | seen = {} 46 | scan_module(sys.argv[1], 0, seen) 47 | 48 | for k, v in seen.items(): 49 | target = sys.argv[2] + "/" + k 50 | print("Copy", v, "to", target) 51 | shutil.copyfile(v, target) 52 | -------------------------------------------------------------------------------- /scripts/copy-licences.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # (C) Copyright 2024- ECMWF. 4 | # 5 | # This software is licensed under the terms of the Apache Licence Version 2.0 6 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 7 | # 8 | # In applying this licence, ECMWF does not waive the privileges and immunities 9 | # granted to it by virtue of its status as an intergovernmental organisation nor 10 | # does it submit to any jurisdiction. 11 | # 12 | 13 | import json 14 | import re 15 | import sys 16 | 17 | import requests 18 | from html2text import html2text 19 | 20 | 21 | def identity(x): 22 | return x 23 | 24 | 25 | ENTRIES = { 26 | "libeccodes": { 27 | "home": "https://github.com/ecmwf/eccodes", 28 | "copying": "https://raw.githubusercontent.com/ecmwf/eccodes/develop/LICENSE", 29 | }, 30 | "libpng": { 31 | "home": "https://github.com/glennrp/libpng", 32 | "copying": "https://raw.githubusercontent.com/glennrp/libpng/libpng16/LICENSE", 33 | }, 34 | "libaec": { 35 | "home": "https://github.com/MathisRosenhauer/libaec", 36 | "copying": "https://raw.githubusercontent.com/MathisRosenhauer/libaec/master/LICENSE.txt", 37 | }, 38 | "libjasper": { 39 | "home": "https://github.com/jasper-software/jasper", 40 | "copying": "https://raw.githubusercontent.com/jasper-software/jasper/master/LICENSE.txt", 41 | }, 42 | "libopenjp2": { 43 | "home": "https://github.com/uclouvain/openjpeg", 44 | "copying": "https://raw.githubusercontent.com/uclouvain/openjpeg/master/LICENSE", 45 | }, 46 | "libopenjpeg": { 47 | "home": "https://code.google.com/archive/p/openjpeg/", 48 | "copying": "https://raw.githubusercontent.com/uclouvain/openjpeg/master/LICENSE", 49 | }, 50 | "libjpeg": { 51 | "home": "http://ijg.org", 52 | "copying": "https://jpegclub.org/reference/libjpeg-license/", 53 | "html": True, 54 | }, 55 | "libzlib1": { 56 | "home": "https://www.zlib.net/", 57 | "copying": "https://www.zlib.net/zlib_license.html", 58 | "html": True, 59 | }, 60 | } 61 | 62 | PATTERNS = { 63 | r"^libpng\d+$": "libpng", 64 | r"^libproj(_\d+)+$": "libproj", 65 | } 66 | 67 | ALIASES = { 68 | "libeccodes_memfs": "libeccodes", 69 | } 70 | 71 | if False: 72 | for e in ENTRIES.values(): 73 | if isinstance(e, dict): 74 | copying = e["copying"] 75 | if copying.startswith("http"): 76 | requests.head(copying).raise_for_status() 77 | 78 | libs = {} 79 | missing = [] 80 | seen = set() 81 | 82 | for line in open(sys.argv[1], "r"): # noqa: C901 83 | lib = "-no-regex-" 84 | lib = line.strip().split("/")[-1] 85 | lib = lib.split("-")[0].split(".")[0] 86 | 87 | if lib == "": 88 | continue 89 | 90 | if not lib.startswith("lib"): 91 | lib = f"lib{lib}" 92 | 93 | for k, v in PATTERNS.items(): 94 | if re.match(k, lib): 95 | lib = v 96 | 97 | lib = ALIASES.get(lib, lib) 98 | 99 | if lib not in ENTRIES: 100 | missing.append((lib, line)) 101 | continue 102 | 103 | if lib in seen: 104 | continue 105 | 106 | seen.add(lib) 107 | 108 | e = ENTRIES[lib] 109 | if e is None: 110 | continue 111 | 112 | libs[lib] = dict(path=f"copying/{lib}.txt", home=e["home"]) 113 | copying = e["copying"] 114 | 115 | filtering = identity 116 | if e.get("html"): 117 | filtering = html2text 118 | 119 | with open(f"eccodes/copying/{lib}.txt", "w") as f: 120 | if copying.startswith("http://") or copying.startswith("https://"): 121 | r = requests.get(copying) 122 | r.raise_for_status() 123 | for n in filtering(r.text).split("\n"): 124 | print(n, file=f) 125 | else: 126 | for n in copying.split("\n"): 127 | print(n, file=f) 128 | 129 | with open("eccodes/copying/list.json", "w") as f: 130 | print(json.dumps(libs), file=f) 131 | 132 | 133 | assert len(missing) == 0, json.dumps(missing, indent=4, sort_keys=True) 134 | -------------------------------------------------------------------------------- /scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | html2text 3 | -------------------------------------------------------------------------------- /scripts/select-python-linux.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # (C) Copyright 2024- ECMWF. 3 | # 4 | # This software is licensed under the terms of the Apache Licence Version 2.0 5 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | # In applying this licence, ECMWF does not waive the privileges and immunities 7 | # granted to it by virtue of its status as an intergovernmental organisation 8 | # nor does it submit to any jurisdiction. 9 | 10 | set -xe 11 | version=$1 12 | 13 | source /opt/conda/etc/profile.d/conda.sh 14 | 15 | CONDA_PY_ENV_DIR=$RUNNER_TEMP/venv_$version 16 | 17 | if [ ! -d "${CONDA_PY_ENV_DIR}" ]; then 18 | conda create -y -p $CONDA_PY_ENV_DIR 19 | fi 20 | 21 | conda activate $CONDA_PY_ENV_DIR 22 | conda install -y python=$version openldap 23 | 24 | which python3 25 | python3 --version 26 | -------------------------------------------------------------------------------- /scripts/select-python-macos.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # (C) Copyright 2024- ECMWF. 3 | # 4 | # This software is licensed under the terms of the Apache Licence Version 2.0 5 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | # In applying this licence, ECMWF does not waive the privileges and immunities 7 | # granted to it by virtue of its status as an intergovernmental organisation 8 | # nor does it submit to any jurisdiction. 9 | 10 | set -xe 11 | version=$1 12 | 13 | P_PATH=$(brew --prefix --installed python@$version)/libexec/bin 14 | PATH=$P_PATH:$PATH 15 | 16 | # temporarily do not fail on unbound env vars so that this script can work outside GitHub Actions 17 | set +u 18 | if [ ! -z "${GITHUB_ACTION}" ]; then 19 | echo $P_PATH >> $GITHUB_PATH 20 | fi 21 | set -u 22 | 23 | echo Python version $1 at $P_PATH 24 | -------------------------------------------------------------------------------- /scripts/test-linux.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # (C) Copyright 2024- ECMWF. 3 | # 4 | # This software is licensed under the terms of the Apache Licence Version 2.0 5 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | # In applying this licence, ECMWF does not waive the privileges and immunities 7 | # granted to it by virtue of its status as an intergovernmental organisation 8 | # nor does it submit to any jurisdiction. 9 | 10 | set -eaux 11 | python_version=$1 12 | 13 | ls -l 14 | source ./scripts/select-python-linux.sh ${python_version} 15 | 16 | echo $PATH 17 | pwd 18 | ls -l 19 | 20 | pip install *.whl 21 | pip install pytest 22 | pip install -r tests/requirements.txt 23 | pip freeze 24 | 25 | ls -l $RUNNER_TEMP/venv_$version/lib/python${python_version}/site-packages/eccodes.libs/ 26 | 27 | cd tests 28 | ECCODES_PYTHON_TRACE_LIB_SEARCH=1 pytest -v -s 29 | 30 | rm -fr *.whl tests -------------------------------------------------------------------------------- /scripts/test-macos.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # (C) Copyright 2024- ECMWF. 3 | # 4 | # This software is licensed under the terms of the Apache Licence Version 2.0 5 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | # In applying this licence, ECMWF does not waive the privileges and immunities 7 | # granted to it by virtue of its status as an intergovernmental organisation 8 | # nor does it submit to any jurisdiction. 9 | 10 | set -eaux 11 | python_version=$1 12 | 13 | VENV_DIR=./dist_venv_${python_version} 14 | 15 | ls -l 16 | source ./scripts/select-python-macos.sh ${python_version} 17 | echo $PATH 18 | 19 | rm -rf ${VENV_DIR} 20 | which python 21 | python --version 22 | python -m venv ${VENV_DIR} 23 | source ${VENV_DIR}/bin/activate 24 | echo $PATH 25 | which python 26 | python --version 27 | 28 | pwd 29 | ls -l 30 | 31 | pip install *.whl 32 | pip install pytest 33 | pip install -r tests/requirements.txt 34 | pip freeze 35 | 36 | cd tests 37 | ECCODES_PYTHON_TRACE_LIB_SEARCH=1 pytest -v -s 38 | 39 | rm -fr *.whl tests -------------------------------------------------------------------------------- /scripts/versions.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # (C) Copyright 2024- ECMWF. 3 | # 4 | # This software is licensed under the terms of the Apache Licence Version 2.0 5 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | # In applying this licence, ECMWF does not waive the privileges and immunities 7 | # granted to it by virtue of its status as an intergovernmental organisation 8 | # nor does it submit to any jurisdiction. 9 | 10 | set -eaux 11 | cat versions 12 | cd src 13 | 14 | for n in * 15 | do 16 | cd $n 17 | url=$(git remote -v | head -1 | awk '{print $2;}') 18 | sha1=$(git rev-parse HEAD) 19 | echo git $url $sha1 20 | cd .. 21 | done 22 | 23 | cd .. 24 | -------------------------------------------------------------------------------- /scripts/wheel-linux.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # (C) Copyright 2024- ECMWF. 3 | # 4 | # This software is licensed under the terms of the Apache Licence Version 2.0 5 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | # In applying this licence, ECMWF does not waive the privileges and immunities 7 | # granted to it by virtue of its status as an intergovernmental organisation 8 | # nor does it submit to any jurisdiction. 9 | 10 | set -eaux 11 | 12 | # ensure the cleanup task can delete our workspace 13 | umask 0000 14 | chmod -R a+w . 15 | 16 | version=$(echo $1| sed 's/\.//') 17 | 18 | TOPDIR=$(/bin/pwd) 19 | 20 | LD_LIBRARY_PATH=$TOPDIR/install/lib:$TOPDIR/install/lib64:$LD_LIBRARY_PATH 21 | 22 | sudo /opt/python/cp${version}-cp${version}/bin/pip3 install 'setuptools>=72.1.0' 23 | /opt/python/cp${version}-cp${version}/bin/pip3 list 24 | 25 | rm -fr dist wheelhouse 26 | /opt/python/cp${version}-cp${version}/bin/python3 setup.py --binary-wheel bdist_wheel 27 | 28 | # Do it twice to get the list of libraries 29 | 30 | auditwheel repair dist/*.whl 31 | unzip -l wheelhouse/*.whl 32 | unzip -l wheelhouse/*.whl | grep 'eccodes.libs/' > libs 33 | 34 | pip3 install -r scripts/requirements.txt 35 | python3 ./scripts/copy-licences.py libs 36 | 37 | rm -fr dist wheelhouse 38 | /opt/python/cp${version}-cp${version}/bin/python3 setup.py --binary-wheel bdist_wheel 39 | auditwheel repair dist/*.whl 40 | rm -fr dist 41 | -------------------------------------------------------------------------------- /scripts/wheel-macos.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # (C) Copyright 2024- ECMWF. 3 | # 4 | # This software is licensed under the terms of the Apache Licence Version 2.0 5 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | # In applying this licence, ECMWF does not waive the privileges and immunities 7 | # granted to it by virtue of its status as an intergovernmental organisation 8 | # nor does it submit to any jurisdiction. 9 | 10 | set -eaux 11 | python_version=$1 12 | 13 | arch=$(arch) 14 | [[ $arch == "i386" ]] && arch="x86_64" # GitHub Actions on macOS declare i386 15 | 16 | ARCH="arch -$arch" 17 | 18 | diet() { 19 | 20 | if [[ $arch == "x86_64" ]]; then 21 | return 22 | fi 23 | 24 | # Remove the architectures we don't need 25 | 26 | echo ================================================================= 27 | pwd 28 | cd dist 29 | pwd 30 | name=$(ls -1 *.whl) 31 | echo $name 32 | unzip *.whl 33 | ls -l 34 | cd eccodes 35 | ls -l 36 | so=$(ls -1 *.so) 37 | echo "$so" 38 | 39 | lipo -info $so 40 | lipo -thin $arch $so -output $so.$arch 41 | mv $so.$arch $so 42 | lipo -info $so 43 | cd .. 44 | pwd 45 | zip -r $name eccodes 46 | cd .. 47 | 48 | echo ================================================================= 49 | pwd 50 | 51 | ls -l dist 52 | } 53 | 54 | # version=$(echo $1| sed 's/\.//') 55 | env | sort 56 | 57 | source scripts/select-python-macos.sh $python_version 58 | 59 | 60 | pip3 list 61 | brew list 62 | 63 | # set up virtualenv 64 | $ARCH python -m venv ./dist_venv_${python_version} 65 | source ./dist_venv_${python_version}/bin/activate 66 | 67 | pip list 68 | brew list 69 | 70 | pip3 install wheel delocate setuptools pytest 71 | 72 | rm -fr dist wheelhouse tmp 73 | $ARCH python setup.py --binary-wheel bdist_wheel 74 | 75 | #IR diet 76 | 77 | name=$(ls -1 dist/*.whl) 78 | newname=$(echo $name | sed "s/_universal2/_${arch}/") 79 | echo $name $newname 80 | 81 | # Do it twice to get the list of libraries 82 | $ARCH delocate-wheel -w wheelhouse dist/*.whl 83 | unzip -l wheelhouse/*.whl | grep 'dylib' >libs 84 | pip3 install -r scripts/requirements.txt 85 | python ./scripts/copy-licences.py libs 86 | 87 | DISTUTILS_DEBUG=1 88 | 89 | rm -fr dist wheelhouse 90 | $ARCH python setup.py --binary-wheel bdist_wheel # --plat-name $arch 91 | #IR diet 92 | 93 | # mv dist/$name $newname 94 | # find dist/*.dist-info -print 95 | 96 | $ARCH delocate-wheel -w wheelhouse dist/*.whl 97 | 98 | # test the wheel 99 | pip install --force-reinstall wheelhouse/*.whl 100 | cd tests 101 | pytest -------------------------------------------------------------------------------- /scripts/wheel-windows.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # (C) Copyright 2024- ECMWF. 3 | # 4 | # This software is licensed under the terms of the Apache Licence Version 2.0 5 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 6 | # In applying this licence, ECMWF does not waive the privileges and immunities 7 | # granted to it by virtue of its status as an intergovernmental organisation 8 | # nor does it submit to any jurisdiction. 9 | 10 | set -eaux 11 | 12 | 13 | pip install wheel setuptools 14 | 15 | rm -fr dist wheelhouse ecmwflibs.egg-info build 16 | python setup.py --binary-wheel bdist_wheel 17 | mv dist wheelhouse 18 | 19 | ls -l wheelhouse 20 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test = pytest 3 | 4 | [tool:pytest] 5 | norecursedirs = 6 | build 7 | dist 8 | .tox 9 | .docker-tox 10 | .eggs 11 | pep8maxlinelength = 99 12 | mccabe-complexity = 11 13 | filterwarnings = 14 | ignore::FutureWarning 15 | pep8ignore = 16 | * E203 W503 17 | */__init__.py E402 18 | eccodes/eccodes.py ALL 19 | gribapi/errors.py ALL 20 | gribapi/gribapi.py E501 21 | flakes-ignore = 22 | */__init__.py UnusedImport 23 | */__init__.py ImportStarUsed 24 | eccodes/eccodes.py ALL 25 | eccodes/high_level/* ALL 26 | gribapi/errors.py ALL 27 | 28 | [coverage:run] 29 | branch = True 30 | 31 | [zest.releaser] 32 | python-file-with-version = gribapi/bindings.py 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # (C) Copyright 2017- ECMWF. 4 | # 5 | # This software is licensed under the terms of the Apache Licence Version 2.0 6 | # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 7 | # 8 | # In applying this licence, ECMWF does not waive the privileges and immunities 9 | # granted to it by virtue of its status as an intergovernmental organisation nor 10 | # does it submit to any jurisdiction. 11 | # 12 | 13 | import io 14 | import os 15 | import re 16 | import sys 17 | 18 | import setuptools 19 | 20 | 21 | def read(path): 22 | file_path = os.path.join(os.path.dirname(__file__), *path.split("/")) 23 | return io.open(file_path, encoding="utf-8").read() 24 | 25 | 26 | # single-sourcing the package version using method 1 of: 27 | # https://packaging.python.org/guides/single-sourcing-package-version/ 28 | def parse_version_from(path): 29 | version_pattern = ( 30 | r"^__version__ = [\"\'](.*)[\"\']" # More permissive regex pattern 31 | ) 32 | version_file = read(path) 33 | version_match = re.search(version_pattern, version_file, re.M) 34 | if version_match is None or len(version_match.groups()) > 1: 35 | raise ValueError("couldn't parse version") 36 | return version_match.group(1) 37 | 38 | 39 | # for the binary wheel 40 | libdir = os.path.realpath("install/lib") 41 | incdir = os.path.realpath("install/include") 42 | libs = ["eccodes"] 43 | 44 | if "--binary-wheel" in sys.argv: 45 | sys.argv.remove("--binary-wheel") 46 | 47 | # https://setuptools.pypa.io/en/latest/userguide/ext_modules.html 48 | ext_modules = [ 49 | setuptools.Extension( 50 | "eccodes._eccodes", 51 | sources=["eccodes/_eccodes.cc"], 52 | language="c++", 53 | libraries=libs, 54 | library_dirs=[libdir], 55 | include_dirs=[incdir], 56 | extra_link_args=["-Wl,-rpath," + libdir], 57 | ) 58 | ] 59 | 60 | def shared(directory): 61 | result = [] 62 | for path, dirs, files in os.walk(directory): 63 | for f in files: 64 | result.append(os.path.join(path, f)) 65 | return result 66 | 67 | # Paths must be relative to package directory... 68 | shared_files = ["versions.txt"] 69 | shared_files += [x[len("eccodes/") :] for x in shared("eccodes/copying")] 70 | 71 | if os.name == "nt": 72 | for n in os.listdir("eccodes"): 73 | if n.endswith(".dll"): 74 | shared_files.append(n) 75 | 76 | else: 77 | ext_modules = [] 78 | shared_files = [] 79 | 80 | 81 | install_requires = ["numpy"] 82 | if sys.version_info < (3, 7): 83 | install_requires = ["numpy<1.20"] 84 | elif sys.version_info < (3, 8): 85 | install_requires = ["numpy<1.22"] 86 | elif sys.version_info < (3, 9): 87 | install_requires = ["numpy<1.25"] 88 | 89 | install_requires += ["attrs", "cffi", "findlibs"] 90 | 91 | setuptools.setup( 92 | name="eccodes", 93 | version=parse_version_from("gribapi/bindings.py"), 94 | description="Python interface to the ecCodes GRIB and BUFR decoder/encoder", 95 | long_description=read("README.rst") + read("CHANGELOG.rst"), 96 | author="European Centre for Medium-Range Weather Forecasts (ECMWF)", 97 | author_email="software.support@ecmwf.int", 98 | license="Apache License Version 2.0", 99 | url="https://github.com/ecmwf/eccodes-python", 100 | packages=setuptools.find_packages(), 101 | include_package_data=True, 102 | package_data={"": shared_files}, 103 | install_requires=install_requires, 104 | tests_require=[ 105 | "pytest", 106 | "pytest-cov", 107 | "pytest-flakes", 108 | ], 109 | test_suite="tests", 110 | zip_safe=True, 111 | keywords="ecCodes GRIB BUFR", 112 | classifiers=[ 113 | "Development Status :: 4 - Beta", 114 | "Intended Audience :: Developers", 115 | "License :: OSI Approved :: Apache Software License", 116 | "Programming Language :: Python :: 3.9", 117 | "Programming Language :: Python :: 3.10", 118 | "Programming Language :: Python :: 3.11", 119 | "Programming Language :: Python :: 3.12", 120 | "Programming Language :: Python :: 3.13", 121 | "Programming Language :: Python :: Implementation :: CPython", 122 | "Programming Language :: Python :: Implementation :: PyPy", 123 | "Operating System :: OS Independent", 124 | ], 125 | ext_modules=ext_modules, 126 | ) 127 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | -------------------------------------------------------------------------------- /tests/sample-data/era5-levels-members.grib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecmwf/eccodes-python/1b1e4a0231a3a34eeb7a285d5138dfe3b4cd4235/tests/sample-data/era5-levels-members.grib -------------------------------------------------------------------------------- /tests/sample-data/tiggelam_cnmc_sfc.grib2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecmwf/eccodes-python/1b1e4a0231a3a34eeb7a285d5138dfe3b4cd4235/tests/sample-data/tiggelam_cnmc_sfc.grib2 -------------------------------------------------------------------------------- /tests/test_20_main.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from eccodes import __main__ 4 | 5 | 6 | def test_main(capsys): 7 | __main__.main(argv=["selfcheck"]) 8 | stdout, _ = capsys.readouterr() 9 | 10 | assert "Your system is ready." in stdout 11 | 12 | with pytest.raises(RuntimeError): 13 | __main__.main(argv=["non-existent-command"]) 14 | -------------------------------------------------------------------------------- /tests/test_20_messages.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | # flake8: noqa 7 | 8 | 9 | # from eccodes import messages 10 | 11 | SAMPLE_DATA_FOLDER = os.path.join(os.path.dirname(__file__), "sample-data") 12 | TEST_DATA = os.path.join(SAMPLE_DATA_FOLDER, "era5-levels-members.grib") 13 | 14 | 15 | def _test_Message_read(): 16 | with open(TEST_DATA) as file: 17 | res1 = messages.Message.from_file(file) 18 | 19 | assert res1.message_get("paramId") == 129 20 | assert res1["paramId"] == 129 21 | assert list(res1)[0] == "globalDomain" 22 | assert list(res1.message_grib_keys("time"))[0] == "dataDate" 23 | assert "paramId" in res1 24 | assert len(res1) > 100 25 | 26 | with pytest.raises(KeyError): 27 | res1["non-existent-key"] 28 | 29 | assert res1.message_get("non-existent-key", default=1) == 1 30 | 31 | res2 = messages.Message.from_message(res1) 32 | for (k2, v2), (k1, v1) in zip(res2.items(), res1.items()): 33 | assert k2 == k1 34 | if isinstance(v2, np.ndarray) or isinstance(v1, np.ndarray): 35 | assert np.allclose(v2, v1) 36 | else: 37 | assert v2 == v1 38 | 39 | with open(TEST_DATA) as file: 40 | with pytest.raises(EOFError): 41 | while True: 42 | messages.Message.from_file(file) 43 | 44 | 45 | def _test_Message_write(tmpdir): 46 | res = messages.Message.from_sample_name("regular_ll_pl_grib2") 47 | assert res["gridType"] == "regular_ll" 48 | 49 | res.message_set("Ni", 20) 50 | assert res["Ni"] == 20 51 | 52 | res["iDirectionIncrementInDegrees"] = 1.0 53 | assert res["iDirectionIncrementInDegrees"] == 1.0 54 | 55 | res.message_set("gridType", "reduced_gg") 56 | assert res["gridType"] == "reduced_gg" 57 | 58 | res["pl"] = [2.0, 3.0] 59 | assert np.allclose(res["pl"], [2.0, 3.0]) 60 | 61 | # warn on errors 62 | res["centreDescription"] = "DUMMY" 63 | assert res["centreDescription"] != "DUMMY" 64 | res["edition"] = -1 65 | assert res["edition"] != -1 66 | 67 | # ignore errors 68 | res.errors = "ignore" 69 | res["centreDescription"] = "DUMMY" 70 | assert res["centreDescription"] != "DUMMY" 71 | 72 | # raise errors 73 | res.errors = "raise" 74 | with pytest.raises(KeyError): 75 | res["centreDescription"] = "DUMMY" 76 | 77 | with pytest.raises(NotImplementedError): 78 | del res["gridType"] 79 | 80 | out = tmpdir.join("test.grib") 81 | with open(str(out), "wb") as file: 82 | res.write(file) 83 | 84 | 85 | def _test_ComputedKeysMessage_read(): 86 | computed_keys = { 87 | "ref_time": (lambda m: str(m["dataDate"]) + str(m["dataTime"]), None), 88 | "error_key": (lambda m: 1 / 0, None), 89 | "centre": (lambda m: -1, lambda m, v: None), 90 | } 91 | with open(TEST_DATA) as file: 92 | res = messages.ComputedKeysMessage.from_file(file, computed_keys=computed_keys) 93 | 94 | assert res["paramId"] == 129 95 | assert res["ref_time"] == "201701010" 96 | assert len(res) > 100 97 | assert res["centre"] == -1 98 | 99 | with pytest.raises(ZeroDivisionError): 100 | res["error_key"] 101 | 102 | 103 | def _test_ComputedKeysMessage_write(): 104 | computed_keys = { 105 | "ref_time": (lambda m: "%s%04d" % (m["dataDate"], m["dataTime"]), None), 106 | "error_key": (lambda m: 1 / 0, None), 107 | "centre": (lambda m: -1, lambda m, v: None), 108 | } 109 | res = messages.ComputedKeysMessage.from_sample_name( 110 | "regular_ll_pl_grib2", computed_keys=computed_keys 111 | ) 112 | res["dataDate"] = 20180101 113 | res["dataTime"] = 0 114 | assert res["ref_time"] == "201801010000" 115 | 116 | res["centre"] = 1 117 | 118 | 119 | def _test_compat_create_exclusive(tmpdir): 120 | test_file = tmpdir.join("file.grib.idx") 121 | 122 | try: 123 | with messages.compat_create_exclusive(str(test_file)): 124 | raise RuntimeError("Test remove") 125 | except RuntimeError: 126 | pass 127 | 128 | with messages.compat_create_exclusive(str(test_file)) as file: 129 | file.write(b"Hi!") 130 | 131 | with pytest.raises(OSError): 132 | with messages.compat_create_exclusive(str(test_file)) as file: 133 | file.write(b"Hi!") 134 | 135 | 136 | def _test_FileIndex(): 137 | res = messages.FileIndex.from_filestream( 138 | messages.FileStream(TEST_DATA), ["paramId"] 139 | ) 140 | assert res["paramId"] == [129, 130] 141 | assert len(res) == 1 142 | assert list(res) == ["paramId"] 143 | assert res.first() 144 | 145 | with pytest.raises(ValueError): 146 | res.getone("paramId") 147 | 148 | with pytest.raises(KeyError): 149 | res["non-existent-key"] 150 | 151 | subres = res.subindex(paramId=130) 152 | 153 | assert subres.get("paramId") == [130] 154 | assert subres.getone("paramId") == 130 155 | assert len(subres) == 1 156 | 157 | 158 | def _test_FileIndex_from_indexpath_or_filestream(tmpdir): 159 | grib_file = tmpdir.join("file.grib") 160 | 161 | with open(TEST_DATA, "rb") as file: 162 | grib_file.write_binary(file.read()) 163 | 164 | # create index file 165 | res = messages.FileIndex.from_indexpath_or_filestream( 166 | messages.FileStream(str(grib_file)), ["paramId"] 167 | ) 168 | assert isinstance(res, messages.FileIndex) 169 | 170 | # read index file 171 | res = messages.FileIndex.from_indexpath_or_filestream( 172 | messages.FileStream(str(grib_file)), ["paramId"] 173 | ) 174 | assert isinstance(res, messages.FileIndex) 175 | 176 | # do not read nor create the index file 177 | res = messages.FileIndex.from_indexpath_or_filestream( 178 | messages.FileStream(str(grib_file)), ["paramId"], indexpath="" 179 | ) 180 | assert isinstance(res, messages.FileIndex) 181 | 182 | # can't create nor read index file 183 | res = messages.FileIndex.from_indexpath_or_filestream( 184 | messages.FileStream(str(grib_file)), 185 | ["paramId"], 186 | indexpath=str(tmpdir.join("non-existent-folder").join("non-existent-file")), 187 | ) 188 | assert isinstance(res, messages.FileIndex) 189 | 190 | # trigger mtime check 191 | grib_file.remove() 192 | with open(TEST_DATA, "rb") as file: 193 | grib_file.write_binary(file.read()) 194 | 195 | res = messages.FileIndex.from_indexpath_or_filestream( 196 | messages.FileStream(str(grib_file)), ["paramId"] 197 | ) 198 | assert isinstance(res, messages.FileIndex) 199 | 200 | 201 | def _test_FileIndex_errors(): 202 | class MyMessage(messages.ComputedKeysMessage): 203 | computed_keys = {"error_key": lambda m: 1 / 0} 204 | 205 | stream = messages.FileStream(TEST_DATA, message_class=MyMessage) 206 | res = messages.FileIndex.from_filestream(stream, ["paramId", "error_key"]) 207 | assert res["paramId"] == [129, 130] 208 | assert len(res) == 2 209 | assert list(res) == ["paramId", "error_key"] 210 | assert res["error_key"] == ["undef"] 211 | 212 | 213 | def _test_FileStream(): 214 | res = messages.FileStream(TEST_DATA) 215 | leader = res.first() 216 | assert len(leader) > 100 217 | assert sum(1 for _ in res) == leader["count"] 218 | assert len(res.index(["paramId"])) == 1 219 | 220 | # __file__ is not a GRIB, but contains the "GRIB" string, so it is a very tricky corner case 221 | res = messages.FileStream(str(__file__)) 222 | with pytest.raises(EOFError): 223 | res.first() 224 | 225 | res = messages.FileStream(str(__file__), errors="ignore") 226 | with pytest.raises(EOFError): 227 | res.first() 228 | 229 | # res = messages.FileStream(str(__file__), errors='raise') 230 | # with pytest.raises(bindings.EcCodesError): 231 | # res.first() 232 | -------------------------------------------------------------------------------- /tests/test_highlevel.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import itertools 3 | import pathlib 4 | 5 | import numpy as np 6 | import pytest 7 | 8 | import eccodes 9 | 10 | SAMPLE_DATA_FOLDER = pathlib.Path(__file__).parent / "sample-data" 11 | TEST_GRIB_DATA = SAMPLE_DATA_FOLDER / "tiggelam_cnmc_sfc.grib2" 12 | TEST_GRIB_DATA2 = SAMPLE_DATA_FOLDER / "era5-levels-members.grib" 13 | 14 | 15 | def test_filereader(): 16 | with eccodes.FileReader(TEST_GRIB_DATA) as reader: 17 | count = len([None for _ in reader]) 18 | assert count == 7 19 | 20 | 21 | def test_read_message(): 22 | with eccodes.FileReader(TEST_GRIB_DATA) as reader: 23 | message = next(reader) 24 | assert isinstance(message, eccodes.GRIBMessage) 25 | 26 | 27 | def test_message_get(): 28 | dummy_default = object() 29 | known_missing = "scaleFactorOfSecondFixedSurface" 30 | with eccodes.FileReader(TEST_GRIB_DATA) as reader: 31 | message = next(reader) 32 | assert message.get("edition") == 2 33 | assert message.get("nonexistent") is None 34 | assert message.get("nonexistent", dummy_default) is dummy_default 35 | assert message.get("centre", ktype=int) == 250 36 | assert message.get("dataType:int") == 11 37 | num_vals = message.get("numberOfValues") 38 | assert message.get_size("values") == num_vals 39 | vals = message.get("values") 40 | assert len(vals) == num_vals 41 | vals2 = message.data 42 | assert np.all(vals == vals2) 43 | assert message["Ni"] == 511 44 | assert message["gridType:int"] == 0 45 | with pytest.raises(KeyError): 46 | message["invalid"] 47 | with pytest.raises(KeyError): 48 | message["gridSpec"] 49 | assert message.get("gridSpec", dummy_default) is dummy_default 50 | # keys set as MISSING 51 | assert message.is_missing(known_missing) 52 | assert message.get(known_missing) is None 53 | assert message.get(known_missing, dummy_default) is dummy_default 54 | with pytest.raises(KeyError): 55 | message[known_missing] 56 | 57 | 58 | def test_message_set_plain(): 59 | missing_key = "scaleFactorOfFirstFixedSurface" 60 | with eccodes.FileReader(TEST_GRIB_DATA) as reader: 61 | message = next(reader) 62 | message.set("centre", "ecmf") 63 | vals = np.arange(message.get("numberOfValues"), dtype=np.float32) 64 | message.set_array("values", vals) 65 | assert np.all(message.get("values") == vals) 66 | message.set("values", vals) 67 | message.set_missing(missing_key) 68 | assert message.get("centre") == "ecmf" 69 | assert np.all(message.get("values") == vals) 70 | assert message.is_missing(missing_key) 71 | 72 | 73 | def test_message_set_dict_with_checks(): 74 | with eccodes.FileReader(TEST_GRIB_DATA) as reader: 75 | message = next(reader) 76 | message.set( 77 | { 78 | "centre": 98, 79 | "numberOfValues": 10, 80 | "shortName": "z", 81 | } 82 | ) 83 | with pytest.raises(TypeError): 84 | message.set("centre", "ecmwf", 2) 85 | with pytest.raises(ValueError): 86 | message.set("stepRange", "0-12") 87 | message.set({"stepType": "max", "stepRange": "0-12"}) 88 | message.set("iDirectionIncrementInDegrees", 1.5) 89 | 90 | 91 | def test_message_set_dict_no_checks(): 92 | with eccodes.FileReader(TEST_GRIB_DATA) as reader: 93 | message = next(reader) 94 | assert message.get("longitudeOfFirstGridPoint") == 344250000 95 | assert message.get("longitudeOfLastGridPoint") == 16125000 96 | message.set("swapScanningX", 1, check_values=False) 97 | assert message.get("longitudeOfFirstGridPoint") == 16125000 98 | assert message.get("longitudeOfLastGridPoint") == 344250000 99 | 100 | 101 | def test_message_iter(): 102 | with eccodes.FileReader(TEST_GRIB_DATA2) as reader: 103 | message = next(reader) 104 | keys = list(message) 105 | assert len(keys) >= 192 106 | assert keys[-1] == "7777" 107 | assert "centre" in keys 108 | assert "shortName" in keys 109 | 110 | keys2 = list(message.keys()) 111 | assert keys == keys2 112 | 113 | items = collections.OrderedDict(message.items()) 114 | assert list(items.keys()) == keys 115 | assert items["shortName"] == "z" 116 | assert items["centre"] == "ecmf" 117 | 118 | values = list(message.values()) 119 | assert values[keys.index("shortName")] == "z" 120 | assert values[keys.index("centre")] == "ecmf" 121 | assert values[-1] == "7777" 122 | 123 | 124 | def test_message_iter_missingvalues(): 125 | missing_key = "level" 126 | with eccodes.FileReader(TEST_GRIB_DATA2) as reader: 127 | message = next(reader) 128 | message[missing_key] = 42 129 | message.set_missing(missing_key) 130 | 131 | assert missing_key not in set(message) 132 | assert missing_key not in set(message.keys()) 133 | assert missing_key not in dict(message.items()) 134 | 135 | 136 | def test_message_copy(): 137 | with eccodes.FileReader(TEST_GRIB_DATA2) as reader: 138 | message = next(reader) 139 | message2 = message.copy() 140 | assert list(message.keys()) == list(message2.keys()) 141 | 142 | 143 | def test_write_message(tmp_path): 144 | with eccodes.FileReader(TEST_GRIB_DATA2) as reader1: 145 | fname = tmp_path / "foo.grib" 146 | written = [] 147 | with open(fname, "wb") as fout: 148 | for message in itertools.islice(reader1, 15): 149 | message.write_to(fout) 150 | written.append(message) 151 | with eccodes.FileReader(fname) as reader2: 152 | for message1, message2 in itertools.zip_longest(written, reader2): 153 | assert message1 is not None 154 | assert message2 is not None 155 | for key in [ 156 | "edition", 157 | "centre", 158 | "typeOfLevel", 159 | "level", 160 | "dataDate", 161 | "stepRange", 162 | "dataType", 163 | "shortName", 164 | "packingType", 165 | "gridType", 166 | "number", 167 | ]: 168 | assert message1[key] == message2[key] 169 | assert np.all(message1.data == message2.data) 170 | 171 | 172 | def test_grib_message_from_samples(): 173 | message = eccodes.GRIBMessage.from_samples("regular_ll_sfc_grib2") 174 | assert message["edition"] == 2 175 | assert message["gridType"] == "regular_ll" 176 | assert message["levtype"] == "sfc" 177 | 178 | 179 | def test_bufr_message_from_samples(): 180 | message = eccodes.BUFRMessage.from_samples("BUFR4") 181 | assert message["edition"] == 4 182 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = docs, py38, py37, py36, py35, pypy3, deps 3 | 4 | [testenv] 5 | passenv = WHEELHOUSE PIP_FIND_LINKS PIP_WHEEL_DIR PIP_INDEX_URL 6 | setenv = PYTHONPATH = {toxinidir} 7 | deps = -r{toxinidir}/ci/requirements-tests.txt 8 | commands = pytest -v --flakes --cache-clear --basetemp={envtmpdir} {posargs} 9 | 10 | [testenv:docs] 11 | deps = -r{toxinidir}/ci/requirements-docs.txt 12 | commands = sphinx-build -W -b html docs build/sphinx/html 13 | 14 | [testenv:qc] 15 | basepython = python3.9 16 | # needed for pytest-cov 17 | usedevelop = true 18 | commands = pytest -v --flakes --pep8 --mccabe --cov=eccodes --doctest-glob="*.rst" --cov-report=html --cache-clear --basetemp={envtmpdir} {posargs} 19 | 20 | [testenv:deps] 21 | deps = 22 | commands = python setup.py test 23 | 24 | [flake8] 25 | ; F401 = imported but unused 26 | ; F405 = may be undefined, or defined from star imports 27 | ; F403 = import * used; unable to detect undefined names 28 | ; W503 = line break before binary operator (set by 'black') 29 | ; ignore = F401,F405,F403,W503 30 | max-line-length = 120 31 | ; exclude = tests/* 32 | max-complexity = 12 33 | ; See https://black.readthedocs.io/en/stable/the_black_code_style.html 34 | extend-ignore = E203 35 | 36 | [isort] 37 | profile=black 38 | --------------------------------------------------------------------------------