Skip to content

Rate limiting for the API, and for scheduling and forecasting triggers in particular - #2306

Open
Flix6x wants to merge 16 commits into
mainfrom
feat/api-rate-limiting
Open

Rate limiting for the API, and for scheduling and forecasting triggers in particular#2306
Flix6x wants to merge 16 commits into
mainfrom
feat/api-rate-limiting

Conversation

@Flix6x

@Flix6x Flix6x commented Jul 13, 2026

Copy link
Copy Markdown
Member

Closes #306. Supersedes the tech spike in #587.

Scheduling is our most expensive operation, and nothing stopped a client from triggering it in a tight loop. This adds rate limiting via Flask-Limiter.

What it does

  • A generous default limit (500 per minute) on the API as a whole, counted per user (per IP address if unauthenticated). The health endpoints are exempt, so that monitoring cannot lock itself out.
  • A stricter trigger limit (10 per 5 minutes) on the endpoints which set expensive computation in motion, which share one budget:
    • POST /assets/<id>/schedules/trigger
    • POST /sensors/<id>/schedules/trigger (deprecated, but still callable)
    • POST /sensors/<id>/forecasts/trigger

Hitting a limit returns a 429 with our usual JSON error shape, plus Retry-After and X-RateLimit-* headers (which also gives clients something to poll on, cf. #645).

The two limits count differently. The default limit counts every request, including the ones we refuse — that is what bounds a client who keeps sending us requests we reject, including bad credentials. The trigger limit only counts triggers we accepted: it exists to protect the computation a trigger sets in motion, and a request we rejected (its payload did not validate, or the asset belongs to someone else) cost us no computation. So a client who made a mistake in their flex-model does not pay for it out of their scheduling budget.

Configuration

Setting Default
RATELIMIT_ENABLED True
FLEXMEASURES_API_DEFAULT_RATE_LIMIT "500 per minute"
FLEXMEASURES_API_TRIGGER_RATE_LIMIT "10 per 5 minutes"
FLEXMEASURES_API_RATE_LIMIT_KEY "account"

How often it is reasonable to re-compute a schedule is a business decision, so the host decides what shares a budget: account (the default — one budget per account, which is how billing tends to work), account+asset (each asset gets its own budget), or user.

Plans

Per the discussion in #306, limits can be set per account by putting the account on a Plan: a bundle of the rate limits and quotas which apply to the accounts assigned to it, so it can be shared as a tier ("Free", "Pro"). A field left unset falls back to the server-wide config setting, so behaviour is unchanged for anyone without a plan. The special value "unlimited" exempts an account from a limit.

flexmeasures add plan --name Pro --trigger-rate-limit "60 per 5 minutes" --rate-limit-key account

Admins put an account on a plan from the account page, which also shows the plan the account is on:

  • plan_id is admin-only on PATCH /api/v3_0/accounts/<id>, like consultancy_account_id.
  • A plan usually reflects a contractual agreement, so rather than editing a plan which accounts are on, retire it: flexmeasures edit plan --name Pro --legacy. A legacy plan keeps applying to the accounts already on it, but is no longer offered when assigning a plan.

Plans also carry quotas (max_users, max_assets, max_clients). Those are not enforced yet — a quota is a cap on rows, checked at creation time, which is a separate enforcement path from a rate limit, and is left to a follow-up.

Counts are kept in the Redis we already connect to, so workers share them. If Redis is unreachable, requests are let through rather than taking the API down.

Notes for the reviewer

  • The blocker from Rate limiting for scheduling triggers #587 is gone. That spike could not decorate a single flask-classful view method, so it decorated the whole SensorAPI and used a cost function which counted 0 for every method except the trigger. flask-classful 0.16 stacks per-method decorators fine, so @limit_triggers() now sits on each trigger endpoint and the workaround is dropped.
  • Flask-Limiter buckets a limit per endpoint unless you tell it not to, which had two consequences worth naming, both fixed here: the trigger limit is now a shared_limit (otherwise a client could double their budget by alternating between the asset and the sensor trigger endpoint), and the default limit is now an application limit, i.e. one budget for the whole API rather than one per endpoint.
  • Flask-Limiter[redis] pins redis<8, so this moves us from redis 8.0.0 to 7.4.1. It still satisfies our own redis>4.5 and works with rq and fakeredis. I preferred the supported version over forcing a combination limits explicitly excludes, but shout if you would rather keep redis 8 and drop the extra.
  • Rate limiting stays initialized during tests, with limits set absurdly high, because Flask-Limiter returns early from init_app when disabled — meaning a test cannot turn it on afterwards, only off. The rate-limiting tests lower the limits themselves.

🤖 Generated with Claude Code

https://claude.ai/code/session_01C2JTqYVFPmJgX7kN2DSvbE

… in particular

Scheduling is our most expensive operation, and nothing stopped a client from
triggering it in a tight loop. A generous default limit now applies to every
endpoint under /api/, and a stricter limit to the endpoints which trigger
schedules and forecasts.

Both limits are configurable, and can be overridden per account through the
"rate_limits" account attribute. What the trigger limit is counted against is a
business decision, so hosts choose: per asset, per account, or per user.

Counts live in the Redis we already connect to. If Redis is unreachable, we let
requests through rather than take the API down with it.

This picks up the tech spike in #587, which worked around flask-classful not
supporting per-method decorators. It does support them now, so the cost-function
workaround is gone.

Closes #306.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2JTqYVFPmJgX7kN2DSvbE
@socket-security

socket-security Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedredis@​8.0.1 ⏵ 7.4.199 +1100100100100
Addedflask-limiter@​4.1.1100100100100100

View full report

@nhoening

Copy link
Copy Markdown
Member

I vote to make rate_limits its own account attribute.
We could do it later, as well.
But why not now?

It's a crucial attribute, and will be checked often I believe parsing attributes is a bit more expensive.

@Flix6x

Flix6x commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

Agreed that it shouldn't stay buried in attributes — but I'd like to go one step further than an account column, because I think we'd regret that shape fairly quickly.

On the performance rationale: I don't think we'd actually gain much there. The cost isn't decoding the JSON — it's loading the Account row at all (current_user.account is a relationship, so it's a query on first touch; the JSONB decode on an already-loaded row is negligible next to that). Every shape pays that cost. So I'd rather justify this on the grounds you gave first in #306: it's a crucial, first-class field, and it deserves to be typed and discoverable rather than free-form. That argument I fully buy.

Why not scalar columns on Account: the moment we want a second budget — and I think we will: max users, max assets, maybe max sensors — each one is a new column plus a new migration. And a per-account column can't express the per-asset differentiation you asked for in #306 ("they might want to rate Assets differently within their account"), which is a natural next ask given that FLEXMEASURES_API_RATE_LIMIT_KEY already defaults to giving each asset its own counter.

So I'd propose the thing you actually suggested in #306: a plan table. Rate limits and quotas are both entitlements; a plan is the right noun for the bundle, and it gives us shared tiers with per-account overrides later without another migration.

I'd also put the key (what a trigger limit is counted against) on the plan, not just the limit value. My first instinct was to keep it as server config, on the grounds that keying is a hosting/protection decision rather than a customer entitlement — but that doesn't hold up: capping how many assets an account may create is just as much a hosting decision, and that's going on the plan too. And putting both on the plan makes it self-contained: "10 per 5 minutes, per asset" is a complete statement of what an account gets, whereas the value alone is only half an entitlement (the effective total is the value times the number of budgets).

Sketch:

class RateLimitKey(enum.Enum):
    ACCOUNT_ASSET = "account+asset"  # each asset gets its own budget
    ACCOUNT = "account"              # the account has one shared budget
    USER = "user"                    # each user gets their own budget


class Plan(db.Model):
    """A bundle of entitlements. Can be shared as a tier (e.g. "Free", "Pro")."""
    id = Column(Integer, primary_key=True)
    name = Column(String(80), unique=True)

    # Rate limits: counted per time window, enforced per request (Redis).
    # NULL means: fall back to the server config.
    default_rate_limit = Column(String(64), nullable=True)   # e.g. "500 per minute"
    trigger_rate_limit = Column(String(64), nullable=True)   # e.g. "10 per 5 minutes"

    # What the trigger limit is counted against. Together with the limit above,
    # this fully specifies the entitlement: "10 per 5 minutes, per asset".
    # Enum rather than free text: this value feeds the rate limiter's key function.
    rate_limit_key = Column(Enum(RateLimitKey), nullable=True)

    # Quotas: static caps on rows, enforced at creation time (SELECT count(*)).
    # NULL means: no cap.
    max_users = Column(Integer, nullable=True)
    max_assets = Column(Integer, nullable=True)


class Account(db.Model, AuthModelMixin):
    ...
    plan_id = Column(Integer, ForeignKey("plan.id"), nullable=True)
    plan = db.relationship("Plan", backref="accounts")

Three things worth flagging explicitly, because they're easy to get wrong:

  1. Rate limits and quotas are not the same animal, even though they belong in the same bundle. A rate limit is a count per time window, checked on every request, answered with a 429. A quota is a cap on rows, checked when you create one, answered with a 403/422. Shared home, completely separate enforcement paths — worth keeping that clear in the code so nobody tries to run max_users through Flask-Limiter.

  2. The key mode becomes user-supplied data feeding a key function. Today it's a config string the host writes; as a plan field it's a DB value. Hence the Enum rather than free text — and the key function should fall back to the config default if it ever sees something unexpected, rather than raise, so that one bad plan row can't turn into a 500 on every request for that account.

  3. FLEXMEASURES_API_RATE_LIMIT_KEY doesn't disappear, it becomes the default for accounts without a plan — and for unauthenticated traffic, which has no account to read a plan from and stays keyed by IP. Same for the limit values: NULL → server config, so behaviour is unchanged for anyone without a plan.

Minor operational note for the docs: changing an account's rate_limit_key mid-window orphans its existing counters in Redis (the old keys simply expire), so a plan change can briefly hand out a fresh budget. Harmless, but surprising if unexplained.

Nothing is released yet, so the migration is trivial and there's no data to move. Happy to do it in this PR, or land the rate limiting as it stands and do the plan table as an immediate follow-up — your call on how much you want to grow this one.

@nhoening

nhoening commented Jul 13, 2026

Copy link
Copy Markdown
Member

I agree with this design.

One thing that came to my mind: For a consultancy account, we might think about a max_clients setting. Also, a consultant should be limited in assigning plans to their clients.

This might need follow-up work, but for now I would make sure the host (admin users?) would be able to choose a default plan for clients (as a config setting, for all consultancy clients, or even per account - but probably the former, so we can get started). Just note that the host account (usually the first to be created on a new FM instance) would treat created consultancy accounts as its own clients.

Replace Account.attributes["rate_limits"] with a first-class Plan
table (Account.plan_id FK), per Nicolas' review on PR #2306. A plan
holds default_rate_limit, trigger_rate_limit and rate_limit_key
(Enum), plus not-yet-enforced quota columns (max_users, max_assets,
max_clients) as groundwork for a follow-up PR.

The trigger key resolution now falls back to the server config (and
ultimately to "account+asset") instead of raising, so a bad key value
can never turn into a 500 on every request for an account.

@nhoening nhoening left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work. I have some smaller requests and questions.

Also, let's bring some of this to the UI. Hosts and users can not see plans, what plan an account is on, nor can they change the plan.
To begin with, I suggest:

  • on the account page, show the plan the account is on
  • admins see a plan dropdown in the account edit form, so they can change it.

Plan management has more details to it so maybe that is a follow-up. For instance, a plan usually represents a contractual agreement. Changing a plan should not be taken too lightly. Instead, the default choice might be to turn a plan into a legacy plan, which cannot be edited (we might decide to add that field in this PR already). Also, plans might have a time limitation, after which the account switches to a new one - but this needs to be discussed, it could be also done by each hosts in their own way.

Comment thread documentation/configuration.rst Outdated
Comment thread documentation/configuration.rst Outdated
Comment thread flexmeasures/api/v3_0/tests/test_rate_limiting.py Outdated
Comment thread flexmeasures/api/v3_0/tests/test_rate_limiting.py Outdated
Comment thread flexmeasures/api/v3_0/tests/test_rate_limiting.py Outdated
…budgets, surface plans in UI and CLI

Nils's review on #2306, plus two bugs it surfaced.

Counting:
- The trigger limit now only deducts when a trigger was accepted (deduct_when),
  so a client whose payload we rejected, or who asked for someone else's asset,
  keeps their scheduling budget. The default limit still counts every request,
  including failed auth, so abuse stays bounded without a second counter.

Keying:
- FLEXMEASURES_API_RATE_LIMIT_KEY now defaults to "account" (how billing works),
  and an unrecognized value falls back to that too.
- "account+asset" now keys on the actual asset, resolving the sensor's asset for
  the deprecated sensor endpoints, rather than on the request path.
- The trigger endpoints now share one budget (shared_limit). They each had their
  own before, so a client could double their budget by alternating endpoints.
- The default limit is now an application limit, i.e. one budget for the whole
  API rather than one per endpoint, which is what its docs already claimed.

Plans:
- Plan.legacy: retire a plan rather than editing one accounts are on.
- Admins can see and set an account's plan on the account page; plan_id is
  admin-only on PATCH /accounts/<id>, and cannot be set to a legacy plan.
- flexmeasures add plan / edit plan, so plans can be created and retired.
  Limit strings are validated on creation rather than at request time.

Tests trigger through the AssetAPI rather than the deprecated sensor endpoint,
and use the RateLimitKey constants throughout.

Signed-off-by: F.N. Claessen <felix@seita.nl>
@Flix6x

Flix6x commented Jul 14, 2026

Copy link
Copy Markdown
Member Author

Thanks — all addressed in 18ac5d5, and your UI ask is in this PR.

Plans in the UI. The account page now shows the plan the account is on, and admins get a plan dropdown in the account edit form. Under it: plan_id is admin-only on PATCH /api/v3_0/accounts/<id>, exactly like consultancy_account_id, and plan changes land in the account audit log.

Legacy plans. I took you up on adding the field now, since nothing is released and the migration is free. Plan.legacy means: the plan keeps applying to the accounts already on it, but it is no longer offered when assigning a plan (the dropdown hides it, and the API refuses it — except that an account already on a legacy plan keeps showing it, marked as such). That gives us the "do not edit a contract, retire it" default you describe, without committing to the rest of plan management yet.

Creating plans. The dropdown needed something to offer, so plans are created and retired from the CLI for now:

flexmeasures add plan --name Pro --trigger-rate-limit "60 per 5 minutes" --rate-limit-key account
flexmeasures edit plan --name Pro --legacy

Limit strings are validated when the plan is created (via the limits parser Flask-Limiter itself uses), so a typo fails at creation rather than at request time.

Left for follow-up, per your read that plan management has more to it: a plans CRUD UI, enforcing the quotas (max_users/max_assets/max_clients — a different enforcement path from rate limits: a cap on rows, checked at creation, answered with a 403/422), a default plan for new client accounts, and time-limited plans. Happy to open an issue capturing those, unless you would rather grow this PR further.

Two bugs also came out of your review, which I have described in the threads: the trigger endpoints each had their own budget instead of sharing one (a client could double their allowance by alternating between the asset and the deprecated sensor endpoint), and the default limit was counted per endpoint rather than across the API. Both fixed.

Resolve conflicts around the accounts API/UI. main moved account-field
validation into AccountPatchSchema and added an account-create endpoint;
this branch's per-account Plan support is ported into that structure:
plan_id becomes a schema-validated field (admin-only, no legacy plans)
rather than a view-level check, with the UI, openapi spec and tests
following suit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: F.N. Claessen <felix@seita.nl>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces API rate limiting (with a stricter shared budget for scheduling/forecasting triggers) backed by Redis via Flask-Limiter, and adds a “Plan” concept to override rate limits (and future quotas) per account/organisation.

Changes:

  • Add Flask-Limiter-based default and trigger-specific rate limits, including consistent 429 JSON errors and rate-limit headers.
  • Introduce Plan (DB model + migration) and wire plan_id into the Accounts API, UI account page, and CLI (flexmeasures add/edit plan).
  • Add documentation and tests covering rate limiting behavior and plan assignment.

Reviewed changes

Copilot reviewed 26 out of 27 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
pyproject.toml Add Flask-Limiter dependency (Redis-backed storage).
flexmeasures/utils/validation_utils.py Add rate-limit string validation and "unlimited" sentinel.
flexmeasures/utils/config_defaults.py Add rate-limiting config defaults and testing overrides.
flexmeasures/ui/views/accounts.py Load assignable plans for the account page (admin-only).
flexmeasures/ui/tests/test_account_crud.py UI tests for plan visibility and admin plan assignment UI.
flexmeasures/ui/templates/accounts/account.html Add plan selector for admins and plan display row.
flexmeasures/ui/static/openapi-specs.json Update generated OpenAPI spec for plan_id on accounts.
flexmeasures/data/schemas/account.py Add plan_id to schemas and validate admin-only + non-legacy plans.
flexmeasures/data/models/user.py Add Plan model and RateLimitKey enum; link Account.plan_id.
flexmeasures/data/migrations/versions/6a767f36151c_add_plan_table_and_account_plan_id.py Create plan table + account.plan_id FK.
flexmeasures/cli/utils.py Add Click callback to validate rate-limit options.
flexmeasures/cli/tests/test_data_add.py Add CLI tests for plan creation/editing and validation.
flexmeasures/cli/data_edit.py Add flexmeasures edit plan command.
flexmeasures/cli/data_add.py Add flexmeasures add plan command.
flexmeasures/api/v3_0/tests/test_rate_limiting.py Add API tests for default/trigger limits, sharing, exemptions, and plan overrides.
flexmeasures/api/v3_0/tests/test_accounts_api.py Add API tests for patching plan_id and plan validation.
flexmeasures/api/v3_0/sensors.py Apply trigger rate limiting decorator to schedule/forecast trigger endpoints.
flexmeasures/api/v3_0/assets.py Apply trigger rate limiting decorator to asset schedule trigger endpoint.
flexmeasures/api/v3_0/accounts.py Document/allow plan_id in PATCH field list.
flexmeasures/api/common/responses.py Add too_many_requests (429) response helper.
flexmeasures/api/common/rate_limiting.py Implement Limiter setup, keying, shared trigger limit, and 429 handler registration.
flexmeasures/api/init.py Register rate limiting during API setup.
documentation/configuration.rst Document rate limiting config, plans, and behavior.
documentation/cli/commands.rst Document new CLI commands: add plan and edit plan.
documentation/changelog.rst Add main changelog entry for rate limiting + plans.
documentation/api/change_log.rst Add API changelog entry for rate limiting semantics and headers.
Comments suppressed due to low confidence (2)

flexmeasures/api/v3_0/tests/test_accounts_api.py:355

  • Please remove the debug print statement from this test; it adds noise to test output and doesn’t assert anything.
    response = client.patch(
        url_for("AccountAPI:patch", id=account.id),
        json={"plan_id": 999999},
    )
    print(f"Response: {response.json}")

flexmeasures/api/v3_0/tests/test_accounts_api.py:373

  • Please remove the debug print statement from this test; it adds noise to test output and doesn’t assert anything.
    response = client.patch(
        url_for("AccountAPI:patch", id=account.id),
        json={"plan_id": legacy_plan.id},
    )
    print(f"Response: {response.json}")

Comment thread flexmeasures/api/v3_0/tests/test_accounts_api.py Outdated
Comment thread flexmeasures/data/schemas/account.py Outdated
Flix6x added 2 commits July 27, 2026 16:56
Semantic resolutions beyond the textual conflicts:
- Keep @limit_triggers() on the asset trigger endpoint, now decorating
  main's AssetTriggerSchemaV3-based signature.
- Trigger endpoints return 202 Accepted since #2224, so the rate-limiting
  tests now expect 202 for accepted triggers. The deduct-on-acceptance
  logic (status < 400) needs no change.
- uv.lock: regenerated from main's version with 'uv lock', re-adding
  Flask-Limiter (and its redis<8 pin) on top of main's lock state.

Signed-off-by: F.N. Claessen <felix@seita.nl>
- The Forbidden message for non-admin plan changes now follows the UI
  terminology guideline (organisation, no internal role names).
- The new plan tests carry the response payload in the assert message
  instead of printing it, so failures stay informative without stdout
  noise.

Signed-off-by: F.N. Claessen <felix@seita.nl>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated 2 comments.

Comment thread flexmeasures/data/schemas/account.py Outdated
Comment thread flexmeasures/data/models/user.py Outdated
The plan_id validator rejected legacy plans unconditionally, but the
account edit form always resends the current plan_id — so an account
already on a legacy plan could no longer save any change. The legacy
check now lives in the PATCH endpoint, where the target account is
known: resubmitting the account's own plan passes, assigning a legacy
plan to any other account still returns 422. Regression test included.

Also pin the RateLimitKey enum's type name in the model to match the
migration, per Copilot's suggestion.

Signed-off-by: F.N. Claessen <felix@seita.nl>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated 4 comments.

Comment thread flexmeasures/utils/validation_utils.py Outdated
Comment thread flexmeasures/ui/views/accounts.py
Comment thread flexmeasures/api/common/rate_limiting.py Outdated
Comment thread documentation/changelog.rst Outdated
- Loading assignable plans now requires update-level admin access, and
  the plan dropdown only renders when there are plans to offer, so
  admin-readers no longer get an assignment control they cannot use.
- The 429 message no longer claims the limit is per endpoint (the
  default limit spans the API; the trigger limit is shared).
- validate_rate_limit: RST docstring per the repo convention, explicit
  return type, and exception chaining preserved with 'from exc'.
- Changelog entry leads with 'organisation' per the UI terminology
  guideline, keeping 'account' only as the technical carrier.

Signed-off-by: F.N. Claessen <felix@seita.nl>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

flexmeasures/api/common/rate_limiting.py:136

  • trigger_limit() can return the special value "unlimited" from a plan. Since this is not a parseable limit string, it risks causing Flask-Limiter parsing errors if the limit callable is evaluated even when exempt_when is true. Returning a parseable limit (and keeping the exemption logic in limit_triggers) avoids that failure mode.
def trigger_limit() -> str:
    return (
        _account_rate_limit("trigger")
        or current_app.config["FLEXMEASURES_API_TRIGGER_RATE_LIMIT"]
    )

Comment thread flexmeasures/api/common/rate_limiting.py Outdated
A plan's 'unlimited' value is an exemption (granted by exempt_when),
not a limit spec, so the limit callables now fall back to the server
config instead of returning it. Previously the behaviour was right,
but only because Flask-Limiter logs and skips an unparseable limit.
Adds a test exempting an account from the default limit, which
exercises exactly this path.

Signed-off-by: F.N. Claessen <felix@seita.nl>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated 1 comment.

Comment thread flexmeasures/cli/data_edit.py
NULL is a meaningful state for every plan field except the name and
the legacy flag (it means: the server-wide behaviour applies), but the
edit command filtered out None values, so a set field could never be
unset again. A repeatable --clear option (mirroring the --null idiom
of 'edit attribute') now does that, and refuses to both set and clear
the same field.

Signed-off-by: F.N. Claessen <felix@seita.nl>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated 1 comment.

Comment thread flexmeasures/api/v3_0/accounts.py Outdated
The endpoint-level 422 was the last ad-hoc {"errors": [...]} response
in this module (the pattern the schema refactor on main removed), so
clients would have needed to special-case it. It now goes through the
unprocessable_entity helper, keyed by field, and the test pins the
envelope shape.

Signed-off-by: F.N. Claessen <felix@seita.nl>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated no new comments.

@Flix6x
Flix6x requested a review from nhoening July 27, 2026 17:31

@nhoening nhoening left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Endpoints which are rate-limited don't seem to be documenting their 429 response yet. Re-generate the OpenAPI JSON afterwards.
  • Please also create a simple flexmeasures show plans command. Show a table with the ID and the supported fields as columns.
  • For flexmeasures edit plan, I suggest that parameter --id is used for identification, so the --name parameter can be used to also edit the plan name.

- Document the 429 response on every rate-limited endpoint. The limiter guards
  these endpoints from outside the views, so no docstring declares this response;
  the specs generator adds it, and knows which endpoints hit the stricter trigger
  limit. Regenerated the OpenAPI JSON.
- Add `flexmeasures show plans`, listing each plan's rate limits and quotas.
- `flexmeasures edit plan` now identifies a plan by `--id`, which frees up
  `--name` to rename the plan.

Signed-off-by: F.N. Claessen <felix@seita.nl>
@Flix6x

Flix6x commented Jul 31, 2026

Copy link
Copy Markdown
Member Author

Thanks! All three points are addressed in 07a5083:

  • 429 in the OpenAPI docs. Rate limiting is applied by the limiter rather than by the views, so no view docstring declares the response — and asking every docstring to remember to say so would go stale. Instead, the specs generator now adds the 429 to each rate-limited operation (document_rate_limits), skipping the health endpoints, which are exempt. The three triggering endpoints get a description that mentions the stricter limit they share. Regenerated the OpenAPI JSON: 51 operations now document it.
  • flexmeasures show plans. Lists ID, name, both rate limits, the rate limit key, the three quotas and whether the plan is legacy, with a line below the table explaining that an empty cell means the server-wide setting applies.
  • flexmeasures edit plan --id. The plan is now identified by ID, so --name renames it (refused, with the offending ID named, if another plan already has that name).

@Flix6x
Flix6x requested a review from nhoening July 31, 2026 08:35
Signed-off-by: Felix Claessen <30658763+Flix6x@users.noreply.github.com>
@Flix6x Flix6x added this to the 1.0.0 milestone Jul 31, 2026
Merging main brought in its own migration (3c2f9e5a1d47), next to this branch's
plan migration (6a767f36151c), which left the branch with two heads. That fails
`flexmeasures db upgrade`, and with it the Docker image build.

Signed-off-by: F.N. Claessen <felix@seita.nl>
Comment thread documentation/configuration.rst Outdated
Comment thread flexmeasures/cli/data_show.py Outdated
Comment thread flexmeasures/cli/data_edit.py Outdated
- Move the rate-limiting explanation and the plan management out of the
  settings list, into host/plans.rst ("Plans and rate-limiting"), which links
  back to the settings. configuration.rst now just lists the settings, like it
  does elsewhere. The new page documents creating, listing, editing and
  retiring plans, and assigning an account to one from the UI or the API.
- Leave the quota fields out of `flexmeasures show plans`, as long as nothing
  enforces them.
- Reword the `edit plan` docstring.

Signed-off-by: F.N. Claessen <felix@seita.nl>
@Flix6x
Flix6x requested a review from nhoening July 31, 2026 15:32

@nhoening nhoening left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rate limiting for scheduling

3 participants