Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
bd84d64
Initial plan
Copilot Feb 10, 2026
61758a6
Add annotation API endpoints for accounts, assets, and sensors
Copilot Feb 10, 2026
9d74567
Fix API issues: add response schema, fix idempotency, add error handling
Copilot Feb 10, 2026
3473aa5
Remove temporary API review file
Copilot Feb 10, 2026
5e4b985
tests(api/dev): add comprehensive tests for annotation API endpoints
Copilot Feb 10, 2026
2c43074
Add comprehensive documentation for annotation API endpoints
Copilot Feb 10, 2026
3fb80b6
Fix missing timezone import in documentation example
Copilot Feb 10, 2026
e830e10
Address code review feedback: improve validation and error handling
Copilot Feb 10, 2026
539b44d
Add changelog entry for annotation API endpoints
Copilot Feb 10, 2026
303ca3d
agents/coordinator: document session 2026-02-10 self-improvement failure
Copilot Feb 10, 2026
8ffaf5a
agents/review-lead: learned successful annotation API implementation …
Copilot Feb 10, 2026
acd51d3
agents/architecture-domain-specialist: learned annotation API pattern…
Copilot Feb 10, 2026
a13d26b
agents/api-backward-compatibility-specialist: learned response schema…
Copilot Feb 10, 2026
575e64d
agents/documentation-developer-experience-specialist: learned feature…
Copilot Feb 10, 2026
9e4581c
agents/test-specialist: learned test execution workflow from session …
Copilot Feb 10, 2026
32137df
agents/review-lead: learned enforcement of agent self-improvement fro…
Copilot Feb 10, 2026
b48d44c
fix: db errors are handled by error handler
Flix6x Feb 10, 2026
9f9fd35
Fix annotation API tests: auth and permissions
Copilot Feb 10, 2026
4c62148
agents/coordinator: tracked recurrence of self-improvement failure pa…
Copilot Feb 10, 2026
6fa2790
agents/test-specialist: learned annotation API patterns from session …
Copilot Feb 10, 2026
811af72
agents/review-lead: learned Click context error pattern from session …
Copilot Feb 10, 2026
cbfbdfc
Merge remote-tracking branch 'origin/copilot/add-feature-request-470'…
Flix6x Feb 10, 2026
3ad8908
agents/review-lead: add mandatory session close checklist from govern…
Copilot Feb 10, 2026
8d67f3c
agents/test-specialist: add full test suite requirement from governan…
Copilot Feb 10, 2026
dfe67e8
agents/tooling-ci: add pre-commit hook enforcement from governance re…
Copilot Feb 10, 2026
9dc7b4a
agents/coordinator: add session close verification pattern from gover…
Copilot Feb 10, 2026
7903f45
Fix linting issues found by pre-commit hooks
Copilot Feb 10, 2026
ef42023
Fix DetachedInstanceError in API tests from improper session handling
Copilot Feb 10, 2026
f0715be
agents/review-lead: learned mandatory checklist enforcement from sess…
Copilot Feb 10, 2026
2b5e8da
style: black
Flix6x Feb 10, 2026
c8cd754
Merge remote-tracking branch 'origin/copilot/add-feature-request-470'…
Flix6x Feb 10, 2026
c728675
style: black
Flix6x Feb 10, 2026
169463a
fix: move new test module to run after test_auth_token
Flix6x Feb 10, 2026
90799eb
Revert "Fix DetachedInstanceError in API tests from improper session …
Flix6x Feb 10, 2026
ef94520
docs: link changelog entry to PR rather than to issue
Flix6x Feb 10, 2026
5cea701
docs: cross-reference API docs in changelog entry
Flix6x Feb 10, 2026
bfe7f7d
docs: add quickrefs
Flix6x Feb 10, 2026
ecc8918
feat: move annotation endpoints from dev blueprint into v3 blueprint
Flix6x Feb 10, 2026
8022c6f
docs: API changelog entry
Flix6x Feb 10, 2026
ff58a01
fix: include annotation endpoints in API v3 docs and openAPI docs
Flix6x Feb 10, 2026
18c8e92
style: plural quickref/tag
Flix6x Feb 10, 2026
746e1c6
style: pluralize API resources
Flix6x Feb 10, 2026
28d8f02
docs: update cross-references
Flix6x Feb 10, 2026
5008f58
remove: Limitations and Roadmap section
Flix6x Feb 10, 2026
d64deb3
docs: move Annotations section from features to concepts
Flix6x Feb 10, 2026
4ed4b79
docs: updates after moving endpoints to v3 blueprint
Flix6x Feb 10, 2026
f51c5b6
docs: no bearer before auth token
Flix6x Feb 10, 2026
d3d3cda
docs: correct permission
Flix6x Feb 10, 2026
b79d8c9
feat: streamline datakey
Flix6x Feb 10, 2026
5227f54
chore: update openapi-specs.json
Flix6x Feb 10, 2026
49bf3de
docs: update annotations section
Flix6x Feb 10, 2026
a54971d
docs: streamline response descriptions
Flix6x Feb 10, 2026
a59a51c
fix: broken cross-reference
Flix6x Feb 10, 2026
e000f71
fix: missing parameters in Swagger
Flix6x Feb 10, 2026
0ece0b9
Merge remote-tracking branch 'origin/main' into copilot/add-feature-r…
Flix6x Feb 10, 2026
14d6e1a
docs: update cross-reference
Flix6x Feb 10, 2026
5065af7
docs: annotations appear less often than imagined
Flix6x Feb 10, 2026
a34533d
Move annotation endpoints to nested resource pattern
Copilot Feb 10, 2026
0dd64c6
Update test URLs to use nested resource pattern
Copilot Feb 10, 2026
e243b8b
Fix formatting with black
Copilot Feb 10, 2026
7e61534
tests/annotations: fix test expectations for non-existent entity erro…
Copilot Feb 10, 2026
8914180
docs: update annotation API endpoints to nested RESTful structure
Copilot Feb 10, 2026
c582a43
agents/review-lead: add mandatory delegation enforcement and regressi…
Copilot Feb 10, 2026
e8476ff
agents/documentation: add endpoint migration documentation guidance
Copilot Feb 10, 2026
ca4976a
agents/coordinator: add Review Lead delegation pattern monitoring
Copilot Feb 10, 2026
45f6201
chore: update openapi-specs.json
Flix6x Feb 11, 2026
bb18a05
docs: update quickref/tags
Flix6x Feb 11, 2026
c958e27
feat: endpoint rst docstrings to OpenAPI html
Flix6x Feb 11, 2026
51a1e3d
docs: cross-reference annotation concept docs in API docs
Flix6x Feb 11, 2026
e93b2cf
refactor: reuse AnnotationSchema for responses, too
Flix6x Feb 11, 2026
f49c8b7
feat: add example values to the AnnotationSchema fields
Flix6x Feb 11, 2026
a4b878c
fix: again get rid of an agent code smell
Flix6x Feb 11, 2026
e2e8b47
feat: improve error message for failing asserts
Flix6x Feb 11, 2026
72bc3d2
refactor: instantiate Annotation object within schema
Flix6x Feb 12, 2026
5d0531a
style: flake8 (obsolete imports)
Flix6x Feb 12, 2026
450633d
feat: prefer single words over snake_case or kebab-case in API fields…
Flix6x Feb 12, 2026
c4433a0
docs: add cross-reference
Flix6x Feb 12, 2026
d3488e0
Merge remote-tracking branch 'origin/main' into copilot/add-feature-r…
Flix6x Feb 12, 2026
5c6e675
docs: correct where annotations are currently displayed
Flix6x Feb 12, 2026
93dee57
docs: do not presume to know best practices
Flix6x Feb 12, 2026
70e09c8
style: do not overly capitalize headers
Flix6x Feb 12, 2026
9a35fd6
fix: update URIs in API changelog entry
Flix6x Feb 12, 2026
81de158
ci: update API specialist lesson
Flix6x Feb 12, 2026
76679ee
feat: clarify distinct IDs by not using the same integer for the anno…
Flix6x Feb 12, 2026
7a9838e
chore: update openapi-specs.json
Flix6x Feb 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 211 additions & 0 deletions .github/agents/api-backward-compatibility-specialist.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,103 @@ Protect FlexMeasures users and integrators by ensuring API changes are backwards
- [ ] **Type changes**: Field type changes are breaking
- [ ] **Validation**: Stricter validation is breaking, looser is safe
- [ ] **Marshmallow schemas**: Check schema version compatibility
- [ ] **Response schema completeness**: Verify all response schemas include required fields (see Response Schema Patterns below)

#### Response Schema Patterns

**Always use separate input and output schemas for API endpoints**

Input schemas validate request data; output schemas control response data. These should be separate classes even if they share fields.

**Checklist for Response Schema Completeness**:

- [ ] **ID field**: Response must include `id` field for created/updated resources
- [ ] **Source field**: If resource has a source, include it in response
- [ ] **Audit fields**: Consider including `created_at`, `updated_at` if relevant
- [ ] **All identifying fields**: Include fields clients need to reference the resource
- [ ] **Idempotency support**: Provide enough data for clients to detect duplicates

**Session 2026-02-10 Case Study** (Annotation API):

**Problem**: Initial annotation API used single schema for input and output:

```python
class AnnotationSchema(Schema):
content = fields.String(required=True)
start = AwareDateTimeField(required=True, format="iso")
# Missing: id field in output
```

**Issue**: Clients couldn't retrieve the `id` of created annotations, breaking idempotency checks.

**Wrong Fix**: Separate input and output schemas:

```python
class AnnotationSchema(Schema):
"""Input schema - validates request data"""
content = fields.String(required=True)
start = AwareDateTimeField(required=True, format="iso")

class AnnotationResponseSchema(Schema):
"""Output schema - includes all data clients need"""
id = fields.Integer(required=True)
content = fields.String(required=True)
start = AwareDateTimeField(required=True, format="iso")
```

**Right Fix**:

```python
class AnnotationResponseSchema(Schema):
"""One schema - validates request data and includes all data clients need.

Please note:
- the use of `dump_only`
- metadata description and example(s) must always be included.
- we prefer single-word data keys over snake_case or kebab-case data keys.
"""
id = fields.Int(
dump_only=True,
metadata=dict(
description="The annotation's ID, which is automatically assigned.",
example=19,
),
)
content = fields.Str(
required=True,
validate=Length(max=1024),
metadata={
"description": "Text content of the annotation (max 1024 characters).",
"examples": [
"Server maintenance",
"Installation upgrade",
"Operation Main Strike",
],
},
)
start = AwareDateTimeField(
required=True,
format="iso",
metadata={
"description": "Start time in ISO 8601 format.",
"example": "2026-02-11T17:52:03+01:00",
},
)
source_id = fields.Int(
data_key="source",
dump_only=True,
metadata=dict(
description="The annotation's data source ID, which usually corresponds to a user (it is not the user ID, though).",
example=21,
),
)
```

**Why This Matters**:
- Clients need `id` to detect if they've already created this annotation
- Clients need `id` to update or delete the annotation later
- Missing fields force clients to make additional API calls
- Breaks RESTful conventions (POST should return created resource)

#### Parameter Format Consistency

Expand Down Expand Up @@ -182,6 +279,120 @@ Configuration: `FLEXMEASURES_PLUGINS`

Known plugins: flexmeasures-client, flexmeasures-weather, flexmeasures-entsoe

### Idempotency Detection Patterns

**Never rely on `obj.id is None` to detect if object is new**

SQLAlchemy may not assign IDs until commit, and the pattern is unreliable.

**Anti-pattern** (Session 2026-02-10):
```python
annotation = get_or_create_annotation(...)
is_new = annotation.id is None # ❌ Unreliable!
if is_new:
return success_response, 201
else:
return success_response, 200
```

**Problem**: `annotation.id` might be `None` even for existing objects before flush/commit.

**Correct pattern**: Make helper functions return explicit indicators:
```python
def get_or_create_annotation(...):
existing = db.session.query(Annotation).filter_by(...).first()
if existing:
return existing, False # (object, was_created)
new_obj = Annotation(...)
db.session.add(new_obj)
return new_obj, True

# In endpoint:
annotation, was_created = get_or_create_annotation(...)
status_code = 201 if was_created else 200
```

**Why This Matters**:
- Idempotency is critical for API reliability
- Wrong status codes break client logic
- Clients depend on 201 vs 200 to track resource creation

### Error Handling Patterns

**Catch specific exceptions, not bare `Exception`**

**Anti-pattern**:
```python
try:
annotation = get_or_create_annotation(...)
except Exception as e: # ❌ Too broad!
return error_response("Failed to create annotation")
```

**Problems**:
- Catches programming errors (AttributeError, TypeError, etc.)
- Hides bugs that should fail loudly
- Makes debugging difficult

**Correct pattern**:
```python
from sqlalchemy.exc import SQLAlchemyError

try:
annotation = get_or_create_annotation(...)
except SQLAlchemyError as e: # ✅ Specific database errors
db.session.rollback()
return error_response("Database error creating annotation", 500)
except ValueError as e: # ✅ Expected validation errors
return error_response(str(e), 400)
# Let other exceptions propagate - they indicate bugs
```

**When to catch what**:
- `SQLAlchemyError`: Database errors (connection, integrity, etc.)
- `ValueError`: Expected validation failures
- `KeyError`: Missing required data
- Don't catch: `AttributeError`, `TypeError`, `NameError` (these are bugs)

### Experimental API Documentation

**Endpoints in `/api/dev/` must warn users about instability**

**Required elements**:

1. **Docstring warning**:
```python
@annotations_bp.route("/annotations", methods=["POST"])
def create_annotation():
"""Create a new annotation.

.. warning::
This endpoint is experimental and may change without notice.
It is not subject to semantic versioning guarantees.
"""
```

2. **Response header** (if applicable):
```python
response.headers["X-API-Stability"] = "experimental"
```

3. **OpenAPI metadata**:
```yaml
/api/dev/annotations:
post:
tags:
- experimental
description: |
⚠️ **Experimental API** - This endpoint may change without notice.
```

**Why This Matters**:
- Users integrating with `/api/dev/` need clear expectations
- Protects maintainers' ability to iterate quickly
- Prevents users from depending on unstable contracts
- Documents the migration path to stable API

### Related Files

- API: `flexmeasures/api/`
Expand Down
101 changes: 101 additions & 0 deletions .github/agents/architecture-domain-specialist.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ This agent owns the integrity of models (e.g. assets, sensors, data sources, sch
- [ ] **Account ownership**: Check that all assets have `account_id` set correctly
- [ ] **Sensor-Asset binding**: Validate sensors are properly linked to assets
- [ ] **TimedBelief structure**: Ensure (event_start, belief_time, source, cumulative_probability) integrity
- [ ] **Annotation relationships**: Verify many-to-many associations use relationship append pattern

### Flex-context & flex-model

Expand All @@ -63,6 +64,7 @@ This agent owns the integrity of models (e.g. assets, sensors, data sources, sch
- [ ] **No quick hacks**: Push back on changes that erode model clarity for short-term gains
- [ ] **Separation of concerns**: Validate, process, and persist should be distinct steps
- [ ] **Multi-tenancy**: Ensure account-level access control is maintained
- [ ] **Idempotency**: API endpoints should use get-or-create patterns with proper tuple returns for status detection

### Schema-Code Consistency

Expand Down Expand Up @@ -233,6 +235,28 @@ fields_to_remove = ["as_job"] # ❌ Wrong format
- **Location**: `/flexmeasures/data/models/data_sources.py`
- **Purpose**: Forecasters and reporters subclass `DataGenerator` to couple configured instances to unique data sources (schedulers are not yet subclassing `DataGenerator`)

#### Annotation
- **Location**: `flexmeasures/data/models/annotations.py`
- **Purpose**: Independent entities for metadata about other domain objects (assets, sensors, accounts)
- **Relationships**: Many-to-many with GenericAsset, Sensor, Account (via association tables)
- **Key fields**: `id`, `content`, `type`, `start`, `end`, `source_id`
- **Pattern**: Use `get_or_create_annotation()` for idempotency
- Returns `(annotation, is_new)` tuple
- `is_new=True` for created, `is_new=False` for existing
- Enables proper HTTP status codes (201 vs 200)
- **Association pattern**: Use SQLAlchemy relationship append
```python
# ✅ Correct: Use relationship append
entity.annotations.append(annotation)

# ❌ Wrong: Manual join table manipulation
# Don't create association table entries directly
```
- **Permission model**: Annotations are independent entities
- Adding annotation to entity requires "update" permission on entity
- Not "create-children" (that's for owned hierarchies like asset→sensor)
- Rationale: Many-to-many relationship, annotation exists independently

### Critical Invariants

1. **Acyclic Asset Trees**
Expand Down Expand Up @@ -296,6 +320,83 @@ fields_to_remove = ["as_job"] # ❌ Wrong format
- **Tight coupling**: API/CLI should not import from each other
- **Bypassing services**: Direct model access from API/CLI
- **Quick hacks**: Temporary solutions that become permanent
- **Manual join table manipulation**: Use SQLAlchemy relationship methods, not direct association table inserts
- **Wrong permission model**: Use "update" for annotations, not "create-children" (which is for owned hierarchies)
- **Idempotency without detection**: get-or-create functions should return `(entity, is_new)` tuple

### Annotation API Pattern (Session 2026-02-10)

When implementing POST endpoints for adding annotations to domain entities:

#### 1. Idempotency Pattern
```python
# ✅ Correct: get_or_create returns tuple
annotation, is_new = get_or_create_annotation(
content=...,
type=...,
source_id=...,
# ... other fields
)

# Return appropriate HTTP status
if is_new:
return make_response({...}, 201) # Created
else:
return make_response({...}, 200) # OK (already exists)
```

#### 2. Relationship Management Pattern
```python
# ✅ Correct: Use SQLAlchemy relationship append
entity.annotations.append(annotation)
db.session.commit()

# ❌ Wrong: Manual join table manipulation
# Don't create rows in association tables directly
```

#### 3. Permission Pattern
For many-to-many relationships like annotations:
- **Use "update" permission** to add annotation to entity
- **Not "create-children"** (that's for owned hierarchies like GenericAsset→Sensor)
- **Rationale**: Annotation is independent entity, can be associated with multiple entities

#### 4. API Endpoint Structure
```python
class AnnotationAPI(FlaskView):
@route("/<resource_id>/annotations", methods=["POST"])
@permission_required_for_context("update", ctx_arg_name="entity")
def post(self, resource_id: int):
# 1. Load and validate entity
entity = get_entity_or_abort(resource_id)

# 2. Validate annotation data (Marshmallow schema)
annotation_data = AnnotationSchema().load(request.json)

# 3. Get or create annotation (idempotency)
annotation, is_new = get_or_create_annotation(**annotation_data)

# 4. Associate with entity (relationship append)
entity.annotations.append(annotation)
db.session.commit()

# 5. Return appropriate status
status_code = 201 if is_new else 200
return make_response(AnnotationSchema().dump(annotation), status_code)
```

#### 5. Review Checklist for Annotation Endpoints
- [ ] Does `get_or_create_annotation()` return `(annotation, is_new)` tuple?
- [ ] Does endpoint return 201 for new, 200 for existing?
- [ ] Does code use `entity.annotations.append()` not manual join table?
- [ ] Does endpoint require "update" permission not "create-children"?
- [ ] Is annotation data validated with Marshmallow schema?
- [ ] Is the association committed to database?

**Related Files**:
- Model: `flexmeasures/data/models/annotations.py`
- API: `flexmeasures/api/v3_0/assets.py`, `flexmeasures/api/v3_0/sensors.py`
- Schema: `flexmeasures/data/schemas/annotations.py`

### Related Files

Expand Down
Loading