🔥 effis-client
Europe's official wildfire data, in three lines of async Python.
Burnt-area perimeters and the official Fire Weather Index from EFFIS, the European Forest Fire Information System of the Copernicus Emergency Management Service. No account. No API key. No silent failures.
The official EFFIS Fire Weather Index over Italy on 1 August 2026, fetched with this library.
Contents
- Why this exists
- What you get
- Install
- Quick start
- How it works
- The MapServer traps it handles
- Burnt-area perimeters
- The Fire Weather Index chain
- Cookbook
- API reference
- Error model
- Who is it for
- Roadmap and ideas
- Contributing
- Support the project
- Data licence and attribution
- License
Why this exists
Wildfires in Europe are getting larger, earlier and further north. The data to study them is public and excellent: EFFIS maps burnt areas from satellite imagery every fire season, and publishes the daily Fire Weather Index for the whole continent. Researchers, civil-protection teams, journalists, insurers and students all need it.
Getting it out programmatically, though, is a trap. The endpoints are MapServer, not GeoServer, and every habit you bring from GeoServer fails differently, usually without an error. You can get every fire ever recorded when you asked for one summer. You can get a raster that says "no fire danger anywhere" when it really means "no data". The HTTP status is 200 either way.
effis-client is the small, boring, well-tested layer that knows these traps,
so your analysis does not inherit them.
Two HTTP calls, nine ways to get them silently wrong. This library exists so you never have to discover them yourself.
What you get
| 🗺️ Burnt-area perimeters | GeoJSON polygons from the EFFIS WFS (nrt.ba.poly), filtered to your bounding box and date range |
| 🌡️ Official Fire Weather Index | The complete Canadian FWI System (FWI, FFMC, DMC, DC, ISI, BUI) as raw float values, sampled at any point |
| 🛡️ No silent failures | XML errors disguised as HTTP 200 raise EffisError; an all-zero "no data" raster returns None, never 0.0 |
| ⚡ Async-first | Built on httpx.AsyncClient, which you inject: your timeouts, retries, proxies and connection pool |
| 🧩 Bring your own HTTP | Every step is also a pure function: build parameters, send them with any client, parse the response |
| 🪶 Tiny footprint | One required dependency (httpx); numpy + rasterio only if you sample FWI rasters |
| ✅ Production quality | mypy --strict, 100 % test coverage, py.typed, trusted publishing to PyPI |
| 🔓 Free and open | Apache-2.0 code, CC BY 4.0 data, no credentials anywhere |
Install
pip install effis-client # burnt-area perimeters
pip install "effis-client[raster]" # + Fire Weather Index sampling (numpy, rasterio)
With uv:
uv add effis-client
uv add "effis-client[raster]"
Requires Python 3.12 or newer.
Quick start
import asyncio
from datetime import date
import httpx
from effis_client import fetch_fwi, fetch_perimeters
async def main() -> None:
async with httpx.AsyncClient(timeout=60) as client:
# Bounding boxes are (west, south, east, north) in EPSG:4326.
perimeters = await fetch_perimeters(
client,
(16.5, 40.3, 16.8, 40.6),
start=date(2021, 1, 1),
end=date(2026, 12, 31),
)
print(len(perimeters.features), "fires since 2021")
fwi = await fetch_fwi(
client,
date(2026, 11, 1),
points=[(16.6, 40.6)], # (lon, lat)
bbox=(15.0, 39.5, 17.5, 41.5),
)
print(fwi) # {(16.6, 40.6): 10.34...}, or None if EFFIS has no data
asyncio.run(main())
That is the whole API surface for most users: two functions.
How it works
flowchart LR
subgraph app["Your application"]
U["your code"]
C["httpx.AsyncClient<br/><i>your timeouts, retries, proxy</i>"]
end
subgraph lib["effis-client"]
FP["fetch_perimeters()"]
FF["fetch_fwi()"]
PP["perimeters_params()<br/>parse_perimeters()"]
WP["fwi_params()<br/>check_tiff()<br/>sample_fwi()"]
E["EffisError"]
end
subgraph effis["EFFIS · Copernicus EMS (MapServer)"]
WFS[("WFS /gwis<br/>nrt.ba.poly")]
WMS[("WMS /effis<br/>mf010.fwi, ffmc, ...")]
end
U --> FP & FF
FP --> PP
FF --> WP
PP -- "GetFeature" --> C
WP -- "GetMap image/tiff" --> C
C --> WFS & WMS
PP -. "XML instead of GeoJSON" .-> E
WP -. "XML instead of GeoTIFF" .-> E
The library is split into two layers on purpose:
- Pure functions that build request parameters and parse responses. No I/O,
trivially testable, usable with
requests,aiohttp, a cache, a queue worker, anything. - Thin
fetch_*coroutines that glue the pure functions to thehttpx.AsyncClientyou pass in. The client is never created or closed by the library, so connection pooling, retries and timeouts stay under your control.
A perimeters request, step by step
sequenceDiagram
autonumber
participant App as your code
participant Lib as fetch_perimeters
participant WFS as EFFIS WFS (MapServer)
App->>Lib: bbox, start, end
Lib->>Lib: perimeters_params(bbox)<br/>lowercase WFS 1.1.0 keys, bbox only
Lib->>WFS: GET /gwis?request=GetFeature&typename=nrt.ba.poly&bbox=...
alt GeoJSON
WFS-->>Lib: 200 FeatureCollection (whole history in bbox)
Lib->>Lib: keep fires whose start date is in [start, end]
Lib->>Lib: truncated = fetched >= max_features
Lib-->>App: Perimeters(features, fetched, truncated)
else MapServer exception
WFS-->>Lib: 200 <ServiceExceptionReport>
Lib-->>App: raise EffisError("code: message")
end
An FWI request, step by step
sequenceDiagram
autonumber
participant App as your code
participant Lib as fetch_fwi
participant WMS as EFFIS WMS (MapServer)
App->>Lib: day, points, bbox, component
Lib->>Lib: fwi_params()<br/>layer mf010.{component}, lat/lon swap for WMS 1.3.0
Lib->>WMS: GET /effis?REQUEST=GetMap&FORMAT=image/tiff&TIME=day
alt GeoTIFF with data
WMS-->>Lib: 200 float32 raster
Lib->>Lib: sample pixel under each (lon, lat)
Lib-->>App: {(lon, lat): value, ...}
else GeoTIFF of all zeros (day outside the forecast)
WMS-->>Lib: 200 raster, every pixel 0
Lib-->>App: None (missing data, not a calm day)
else MapServer exception
WMS-->>Lib: 200 <ServiceExceptionReport>
Lib-->>App: raise EffisError("LayerNotDefined: ...")
end
The MapServer traps it handles
Every row was found the hard way and checked again against the live service on 2 November 2026.
| You try | What actually happens | What effis-client does |
|---|---|---|
/gwis/ows (the GeoServer habit) |
the request hangs | uses /gwis |
a wrong typeName |
the connection closes mid-response, no 404 | uses the verified nrt.ba.poly |
CQL_FILTER for dates |
silently ignored: you get every fire ever | filters dates locally, after the bbox query |
bbox + filter together |
the connection dies | sends only the bbox |
| camelCase WFS parameters | refused by MapServer's WFS 1.1.0 | sends lowercase keys |
ecmwf007.fwi (the layer in the EFFIS docs) |
LayerNotDefined |
uses mf010.<component> |
| WMS 1.3.0 with lon/lat bbox order | the wrong region, quietly | swaps to lat/lon for you |
| any error | HTTP 200 with an XML body | raises EffisError with the code and message |
| FWI for a day past the forecast | HTTP 200 with a raster of zeros | returns None |
The last one is the dangerous one:
An all-zero FWI raster reads as "no fire danger anywhere". It means "no data".
A dashboard or a model that takes it at face value will report calm weather on
the day it has no information at all. effis-client returns None for it, so
the absence of data stays visible.
Burnt-area perimeters
The EFFIS Rapid Damage Assessment maps burnt areas from satellite imagery (MODIS, VIIRS, Sentinel-2) during the fire season. Each feature is a GeoJSON polygon with properties like:
{
"id": "384188",
"fire_id": "384188",
"initialdate": "2012-06-18 01:00:00",
"finaldate": "2012-06-18 01:00:00",
"area": "160"
}
area is in hectares. The start date is read from whichever of
initialdate, firedate, FIREDATE or fire_date the layer carries
(feature_date() does this for you).
Both figures come from a single fetch_perimeters call: one bounding box
returned 924 perimeters spanning 2012 to 2026.
Why dates are filtered locally
MapServer cannot filter by date on the server: CQL_FILTER is ignored and an
OGC filter cannot be combined with bbox. So the library asks for the whole
history inside the bbox and filters locally. In practice this is cheap: an
Italian region is a few hundred perimeters and a few hundred kilobytes.
flowchart TD
A["GetFeature(bbox, maxfeatures=5000)"] --> B{"body is GeoJSON?"}
B -- no --> X["raise EffisError"]
B -- yes --> C["all features in bbox<br/>(every year)"]
C --> D{"start date readable?"}
D -- "yes, in [start, end]" --> K["keep"]
D -- "yes, outside" --> R["drop"]
D -- "no date" --> K2["keep<br/><i>a real perimeter is not dropped</i>"]
C --> T{"fetched >= max_features?"}
T -- yes --> TR["truncated = True<br/><i>shrink the bbox</i>"]
T -- no --> OK["truncated = False"]
truncatedmatters. If the response hitsmax_features(5000 by default), the local date filter worked on a partial set and a missing season could look like "no fires". Whenperimeters.truncatedisTrue, shrink the bbox rather than trusting the gap.
The Fire Weather Index chain
The Canadian Forest Fire Weather Index System is the European standard for fire danger. EFFIS computes it daily on Météo-France weather data and serves each component as a raster. This library asks for those rasters as float32 GeoTIFFs (raw values, not coloured map tiles) and samples them at your points.
flowchart LR
subgraph weather["Daily weather at noon"]
T["temperature"]
H["relative humidity"]
W["wind speed"]
R["24 h rain"]
end
subgraph moisture["Fuel moisture codes"]
FFMC["FFMC<br/>fine fuels<br/><i>hours</i>"]
DMC["DMC<br/>duff layer<br/><i>weeks</i>"]
DC["DC<br/>deep organic<br/><i>months</i>"]
end
subgraph behaviour["Fire behaviour indices"]
ISI["ISI<br/>initial spread"]
BUI["BUI<br/>fuel available"]
FWI(["FWI<br/>fire intensity"])
end
T & H & W & R --> FFMC
T & H & R --> DMC
T & R --> DC
FFMC --> ISI
W --> ISI
DMC & DC --> BUI
ISI & BUI --> FWI
| Component | component= |
What it tracks |
|---|---|---|
| Fine Fuel Moisture Code | "ffmc" |
moisture of litter and fine fuels; how easily a fire ignites |
| Duff Moisture Code | "dmc" |
moisture of loosely compacted organic layers |
| Drought Code | "dc" |
deep, slow-drying organic layers; seasonal drought |
| Initial Spread Index | "isi" |
expected rate of spread |
| Buildup Index | "bui" |
total fuel available for combustion |
| Fire Weather Index | "fwi" (default) |
overall fire intensity; the headline number |
EFFIS classifies FWI into danger classes; for reference:
| FWI | < 5.2 | 5.2 – 11.2 | 11.2 – 21.3 | 21.3 – 38.0 | 38.0 – 50.0 | ≥ 50.0 |
|---|---|---|---|---|---|---|
| Danger | very low | low | moderate | high | very high | extreme |
What the WMS accepts
- Days: any past day, today and the next two. Beyond that you get the
all-zero raster, which
fetch_fwiturns intoNone. - Resolution: you choose the pixel size with
step(degrees, default0.05). The bbox and step set the raster's width and height. - Sea and points outside the bbox: sea pixels are zero on valid days too; a point on land reads its own pixel. Points outside the raster, or on a non-finite pixel, are left out of the result dict.
Cookbook
Load perimeters into GeoPandas
import geopandas as gpd
perimeters = await fetch_perimeters(client, bbox, start=start, end=end)
gdf = gpd.GeoDataFrame.from_features(perimeters.features, crs="EPSG:4326")
gdf["area"] = gdf["area"].astype(float) # hectares
print(gdf.sort_values("area", ascending=False).head())
Save perimeters to a GeoJSON file for QGIS
import json
from pathlib import Path
collection = {"type": "FeatureCollection", "features": perimeters.features}
Path("fires.geojson").write_text(json.dumps(collection))
Every FWI component for a list of weather stations, concurrently
from effis_client import COMPONENTS, fetch_fwi
stations = {"Potenza": (15.80, 40.64), "Matera": (16.60, 40.67), "Policoro": (16.68, 40.21)}
bbox = (15.0, 39.5, 17.5, 41.5)
day = date(2026, 8, 1)
results = await asyncio.gather(
*(fetch_fwi(client, day, list(stations.values()), bbox, component=c) for c in COMPONENTS)
)
for component, values in zip(COMPONENTS, results):
if values is None:
print(component, "no data")
continue
for name, point in stations.items():
print(f"{component:>4} {name:<9} {values.get(point)}")
A daily FWI time series for one season
from datetime import date, timedelta
point = (16.60, 40.67)
days = [date(2026, 6, 1) + timedelta(d) for d in range(92)]
async def one(day: date) -> tuple[date, float | None]:
values = await fetch_fwi(client, day, [point], bbox=(16.0, 40.0, 17.0, 41.0))
return day, None if values is None else values.get(point)
series = await asyncio.gather(*(one(d) for d in days))
Be kind to a free public service: cap concurrency (an asyncio.Semaphore or
httpx.Limits(max_connections=4)) for long runs.
Retries, timeouts and a proxy, all yours
transport = httpx.AsyncHTTPTransport(retries=3) # retries connection failures
async with httpx.AsyncClient(
transport=transport,
timeout=httpx.Timeout(60, connect=10),
limits=httpx.Limits(max_connections=4),
proxy="http://proxy.internal:3128",
) as client:
perimeters = await fetch_perimeters(client, bbox, start=start, end=end)
Bring your own HTTP (sync, requests, a cache...)
Every step the fetch_* functions perform is public and pure:
import requests
from effis_client import (
WFS_URL,
perimeters_params,
parse_perimeters,
WMS_URL,
fwi_params,
check_tiff,
sample_fwi,
)
r = requests.get(WFS_URL, params=perimeters_params(bbox), timeout=60)
r.raise_for_status()
perimeters = parse_perimeters(r.content, start=start, end=end)
r = requests.get(WMS_URL, params=fwi_params(day, bbox, component="isi"), timeout=60)
r.raise_for_status()
isi = sample_fwi(check_tiff(r.headers["content-type"], r.content), points)
This is also the easy way to cache responses on disk, run inside a worker queue, or test your own code against recorded fixtures.
Join perimeters and FWI: what was the danger on the day each fire started?
from effis_client import feature_date
for feature in perimeters.features[:20]:
day = feature_date(feature)
if day is None:
continue
lon, lat = feature["geometry"]["coordinates"][0][0] # first vertex is enough here
bbox = (lon - 0.5, lat - 0.5, lon + 0.5, lat + 0.5)
values = await fetch_fwi(client, day, [(lon, lat)], bbox)
print(day, feature["properties"]["area"], "ha", "FWI:", values and values.get((lon, lat)))
This is the kind of question effis-client was built for: it comes from
Limen, which uses the
perimeters as the truth set for its wildfire backtest.
API reference
All names are importable from the package root.
Perimeters
| Name | Kind | Description |
|---|---|---|
fetch_perimeters(client, bbox, *, start, end, url=WFS_URL, layer=LAYER, max_features=5000) |
async |
Burnt-area perimeters intersecting bbox whose fire started in [start, end] |
Perimeters |
frozen dataclass | features: list[dict], fetched: int (before the date filter), truncated: bool |
perimeters_params(bbox, *, layer, max_features) |
pure | Query parameters of the WFS GetFeature request |
parse_perimeters(content, *, start, end, max_features) |
pure | Parse a response body; raises EffisError on non-GeoJSON |
feature_date(feature) |
pure | The fire's start date, or None |
WFS_URL, LAYER, DEFAULT_MAX_FEATURES |
constants | …/gwis, "nrt.ba.poly", 5000 |
Fire Weather Index
| Name | Kind | Description |
|---|---|---|
fetch_fwi(client, day, points, bbox, *, component="fwi", step=0.05, url=WMS_URL) |
async |
{(lon, lat): value}, or None if EFFIS has no data for day |
fwi_params(day, bbox, *, component, step) |
pure | Query parameters of the WMS GetMap request |
check_tiff(content_type, body) |
pure | Return the body if it is a GeoTIFF, else raise EffisError |
sample_fwi(tiff, points) |
pure | Sample the raster under each point; None if all zeros (needs [raster]) |
COMPONENTS, Component |
constants / type | ("fwi", "ffmc", "dmc", "dc", "isi", "bui") and its Literal |
WMS_URL |
constant | …/effis |
Conventions
- Bounding boxes are
(west, south, east, north)in EPSG:4326 degrees. - Points are
(lon, lat). - Dates are
datetime.date; ranges are inclusive.
Error model
flowchart TD
Call["fetch_perimeters / fetch_fwi"] --> Net{"network OK?"}
Net -- no --> HX["httpx.TransportError<br/>(timeout, connection reset, ...)"]
Net -- yes --> St{"HTTP status"}
St -- "4xx / 5xx" --> HS["httpx.HTTPStatusError"]
St -- 200 --> Body{"body type"}
Body -- "XML exception report" --> EE["EffisError<br/>'LayerNotDefined: ...'"]
Body -- "all-zero raster" --> NONE["None<br/>(no data)"]
Body -- "valid data" --> OK["Perimeters / dict of values"]
Nothing is swallowed. Network and HTTP errors propagate as httpx exceptions;
an error dressed up as a 200 becomes an EffisError carrying MapServer's own
code and message; "no data" is None. An empty result is only ever an empty
result.
from effis_client import EffisError
try:
fwi = await fetch_fwi(client, day, points, bbox)
except EffisError as exc:
log.error("EFFIS refused the request: %s", exc)
except httpx.HTTPError as exc:
log.warning("EFFIS unreachable, will retry: %s", exc)
else:
if fwi is None:
log.info("no FWI published for %s yet", day)
Who is it for
- 🔬 Researchers and data scientists building fire-risk models, validating forecasts or studying burnt-area trends.
- 🚒 Civil protection and forestry agencies wiring official EFFIS data into dashboards and alerting pipelines.
- 🛰️ Earth-observation and GIS developers who need clean polygons and numeric rasters, not map tiles.
- 📰 Data journalists covering wildfire seasons with reproducible numbers.
- 🏦 Insurance and climate-risk analysts quantifying exposure.
- 🎓 Students and educators who want real European fire data in a notebook in minutes.
If you use effis-client, please
open an issue
and tell us what you built. A list of users helps the project and helps other
people find it.
Roadmap and ideas
These are directions, not promises. Each one is a good first contribution, and an issue to discuss it is welcome before a pull request.
- A synchronous wrapper for scripts and notebooks
- Other EFFIS layers: active-fire hotspots, fire danger forecasts
- Optional GeoPandas / xarray helpers behind extras
- A small CLI:
effis perimeters --bbox ... --since 2021 > fires.geojson - Recorded-response test helpers for downstream projects
- A gallery of example notebooks
Have another idea? Open an issue.
Contributing
Issues and pull requests are welcome, from typo fixes to new layers.
git clone https://github.com/agent-engineering-studio/effis-client
cd effis-client
uv sync
make check # ruff, mypy --strict and pytest: the same gate CI runs
- Tests run against recorded EFFIS responses in
tests/fixtures/, so they are fast and do not hit the network. - Commits follow Conventional Commits.
- Found a new MapServer quirk? That is a valuable contribution on its own: open an issue with the request and the response you got.
See CONTRIBUTING.md for details and the CHANGELOG for what changed.
Regenerating the figures
The images in this README come from live EFFIS data and the test fixtures:
uv run python docs/scripts/fetch_data.py /tmp/effis-data
uv run --with matplotlib python docs/scripts/render_images.py /tmp/effis-data docs/images tests/fixtures
Support the project
effis-client is free and maintained in spare time. The most useful things you
can do:
- ⭐ Star the repository. It is the simplest signal that this work matters, and it helps others find it.
- 📣 Share it with a colleague, a research group or a civil-protection team who works with wildfire data.
- 🐛 Report quirks, broken layers and confusing docs.
- 🧑💻 Contribute code, examples or notebooks from the roadmap.
- 📝 Cite it in papers and reports that use it, alongside the EFFIS data citation.
- 🤝 Get in touch if your organisation depends on EFFIS data and wants to support or sponsor further development.
Data licence and attribution
EFFIS data are © European Union, Copernicus Emergency Management Service, published under CC BY 4.0. Anything you publish from them must say so, for example:
Data: European Forest Fire Information System (EFFIS), Copernicus Emergency Management Service, © European Union, CC BY 4.0.
The test fixtures and the figures in this README are EFFIS data under the same licence.
effis-client is an independent open-source project. It is not affiliated
with or endorsed by the European Commission, the Joint Research Centre or the
Copernicus programme.
Extracted from Limen,
which uses the perimeters as the truth set for its wildfire backtest and the
FWI rasters to check its own FWI chain
(fwi-chain).
License
Code: Apache-2.0. Contributions are accepted under the same license.
Metadata
Release files for effis-client 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| effis_client-0.1.1.tar.gz | 29.8 kB | With provenance | Details for effis_client-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| effis_client-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for effis_client-0.1.1-py3-none-any.whl |
Total release size: 51.7 kB
Release files / effis_client-0.1.1.tar.gz
| Download URL | effis_client-0.1.1.tar.gz |
|---|---|
| Size | 29.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
89dd915961066663f610f8396cf52378e64e26d18d066bb2c3dec621a363d939
|
|
BLAKE2b-256 checksum How to use checksums |
945c40ebc49ea9bb93c4d7314a84133bd009d7e11c216c1ac19e6eab6ff8d536
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logRelease files / effis_client-0.1.1-py3-none-any.whl
| Download URL | effis_client-0.1.1-py3-none-any.whl |
|---|---|
| Size | 21.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2a5312fcf37030f9c437960fc64751fd9cc1efa32dc3ecefb10d92e8ec0119fc
|
|
BLAKE2b-256 checksum How to use checksums |
1d31664a54184a052104c5da42a7e31d243b30246a5fad51ce596f584f0c721a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logSecurity for effis-client 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for effis_client-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.