Repository: greyli/apiflask (python)

Issue:
Problem with APIFlask generating docs with flask-apscheduler extension: KeyError: 'scheduler'
### Discussed in https://github.com/greyli/apiflask/discussions/206

<div type='discussions-op-text'>

<sup>Originally posted by **diten** February 13, 2022</sup>
Hi, 
I'm pretty new to APIFlask, but I'm starting to like it, a lot. So really great work you guys/girls.
I think there must be a better way to fix this, I just tell what I have done meanwhile.

I'm facing a small problem, which I sort of fix it, but I just comment it here so if is not anywhere (I can't find anything about it), it's helpful to others.

I had a small app, I converted it to APIFlask, but it uses APScheduler, and it keep crashing.
If I remove APScheduler, everything worked right.
So with a little debugging I got to the culprit.

In `app.py._generate_spec` : 
when it iterates over the rules, the scheduler rules aren't  well managed and crash the spec generation.
(I don't quite get if APScheduler definitions are right o wrong).

So my fix was really fast and ugly, but again, it could help somebody else:

Just before `for rule in rules:` I filtered all the /scheduler rules 
```
        # extract scheduler rules as  a temp fix.
        rules: t.List[t.Any] = list(filter(lambda x: not str(x).startswith('/scheduler'), rules))
```

This let me keep using APIFlask and APscheduler altogether.
Scheduler functionality is obviously not in OAS Specs, but I have tried using `@doc(hide=True)` over the Scheduler definition to try to hide it, first, cause I don't want to show it, and then to see if that help solve the problem, but it didn't worked out.
Nonetheless, Scheduler functionality is there and working perfectly now.

Bye, cheers, Diten.
</div>

The error traceback:

```pytb
Traceback (most recent call last):
  File "/Users/greyli/projects/apiflask/venv/lib/python3.8/site-packages/flask/app.py", line 2073, in wsgi_app
    response = self.full_dispatch_request()
  File "/Users/greyli/projects/apiflask/venv/lib/python3.8/site-packages/flask/app.py", line 1518, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/Users/greyli/projects/apiflask/venv/lib/python3.8/site-packages/flask/app.py", line 1516, in full_dispatch_request
    rv = self.dispatch_request()
  File "/Users/greyli/projects/apiflask/src/apiflask/app.py", line 415, in dispatch_request
    return view_function(*req.view_args.values())  # type: ignore
  File "/Users/greyli/projects/apiflask/src/apiflask/app.py", line 550, in spec
    response = jsonify(self._get_spec('json'))
  File "/Users/greyli/projects/apiflask/src/apiflask/app.py", line 621, in _get_spec
    self._spec = self._generate_spec().to_dict()
  File "/Users/greyli/projects/apiflask/src/apiflask/app.py", line 884, in _generate_spec
    blueprint = self.blueprints[blueprint_name]
KeyError: 'scheduler'
```

Pull request description:
Fix the bp detection for endpoints contain a dot

fixes #211

<!--
If needed, ensure each step in the checklist below is complete. If only docs were changed, these aren't relevant and can be removed.
-->

Checklist:

- [x] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [ ] Add or update relevant docs, in the `docs` folder and in code docstring.
- [x] Add an entry in `CHANGES.md` summarizing the change and linking to the issue and your username.
- [ ] Add `*Version changed*` or `*Version added*` note in any relevant docs and docstring.
- [x] Run `pytest` and `tox`, no tests failed.


Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 101c3b3..7c89bfe 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -9,7 +9,10 @@ Released: --
   to improve the IDE auto-completion ([issue #231][issue_231]).
 - Support customizing OpenAPI securitySchemes and operation security, this makes it
   possible to use any external authentication libraries ([pull #232][pull_232]).
+- Improve the way to detect blueprint from view endpoint to support the endpoints that
+  contain dots ([issue #211][issue_211]).
 
+[issue_211]: https://github.com/greyli/apiflask/issues/211
 [issue_231]: https://github.com/greyli/apiflask/issues/231
 [pull_232]: https://github.com/greyli/apiflask/pull/232
 
diff --git a/src/apiflask/app.py b/src/apiflask/app.py
index 8aeac73..b1b15b0 100644
--- a/src/apiflask/app.py
+++ b/src/apiflask/app.py
@@ -846,10 +846,14 @@ class APIFlask(APIScaffold, Flask):
             blueprint_name: t.Optional[str] = None  # type: ignore
             if '.' in rule.endpoint:
                 blueprint_name: str = rule.endpoint.rsplit('.', 1)[0]  # type: ignore
-                blueprint = self.blueprints[blueprint_name]  # type: ignore
-                if not hasattr(blueprint, 'enable_openapi') or \
-                   not blueprint.enable_openapi:  # type: ignore
-                    continue
+                blueprint = self.blueprints.get(blueprint_name)  # type: ignore
+                if blueprint is None:
+                    # just a normal view with dots in its endpoint, reset blueprint_name
+                    blueprint_name = None
+                else:
+                    if not hasattr(blueprint, 'enable_openapi') or \
+                       not blueprint.enable_openapi:  # type: ignore
+                        continue
             # add a default 200 response for bare views
             if not hasattr(view_func, '_spec'):
                 if self.config['AUTO_200_RESPONSE']:


Test patch:
diff --git a/tests/test_route.py b/tests/test_route.py
index df27c8c..2d227c9 100644
--- a/tests/test_route.py
+++ b/tests/test_route.py
@@ -201,3 +201,14 @@ def test_add_url_rule_with_method_view_as_view(app, client):
     validate_spec(rv.json)
     assert rv.json['paths']['/foo/get']['get']['summary'] == 'Get Foo'
     assert rv.json['paths']['/foo/post']['post']['summary'] == 'Create foo'
+
+
+def test_view_endpoint_contains_dot(app, client):
+    @app.route('/', endpoint='hello.world')
+    def foo():
+        pass
+
+    rv = client.get('/openapi.json')
+    assert rv.status_code == 200
+    validate_spec(rv.json)
+    assert rv.json['paths']['/']['get']