Skip to content

Commit f75893e

Browse files
Resolve nits (#932)
* Add type hints to principal * Add type hints for database access * Correct type hints for command line options * Fix unauthenticated server state * Fix 3.9 TypeHints
1 parent d5775c7 commit f75893e

File tree

8 files changed

+122
-88
lines changed

8 files changed

+122
-88
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ Write the date in place of the "Unreleased" in the case a new version is release
99

1010
- `Composite` structure family to enable direct access to table columns in a single namespace.
1111

12+
### Changed
13+
14+
- Adjust arguments of `print_admin_api_key_if_generated` and rename `print_server_info`
15+
1216
### Maintenance
1317

1418
- Extract API key handling
@@ -18,7 +22,7 @@ Write the date in place of the "Unreleased" in the case a new version is release
1822
- This is a breaking change if setting TILED_SERVER_SECRET_KEYS or TILED_ALLOW_ORIGINS,
1923
TILED_SERVER_SECRET_KEYS is now TILED_SECRET_KEYS and these fields now require passing a json
2024
list e.g. ``TILED_SECRET_KEYS='["one", "two"]'``
21-
25+
- More type hinting
2226

2327
## 0.1.0-b20 (2025-03-07)
2428

tiled/authn_database/connection_pool.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1+
from collections.abc import AsyncGenerator
2+
from typing import Optional
3+
14
from fastapi import Depends
2-
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
5+
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
36

4-
from ..server.settings import Settings, get_settings
7+
from ..server.settings import DatabaseSettings, Settings, get_settings
58
from ..utils import ensure_specified_sql_driver
69

710
# A given process probably only has one of these at a time, but we
811
# key on database_settings just case in some testing context or something
912
# we have two servers running in the same process.
10-
_connection_pools = {}
13+
_connection_pools: dict[DatabaseSettings, AsyncEngine] = {}
1114

1215

13-
def open_database_connection_pool(database_settings):
16+
def open_database_connection_pool(database_settings: DatabaseSettings) -> AsyncEngine:
1417
connect_args = {}
1518
kwargs = {} # extra kwargs passed to create_engine
1619
# kwargs["pool_size"] = database_settings.pool_size
@@ -25,13 +28,15 @@ def open_database_connection_pool(database_settings):
2528
return engine
2629

2730

28-
async def close_database_connection_pool(database_settings):
31+
async def close_database_connection_pool(database_settings: DatabaseSettings):
2932
engine = _connection_pools.pop(database_settings, None)
3033
if engine is not None:
3134
await engine.dispose()
3235

3336

34-
async def get_database_engine(settings: Settings = Depends(get_settings)):
37+
async def get_database_engine(
38+
settings: Settings = Depends(get_settings),
39+
) -> AsyncEngine:
3540
# Special case for single-user mode
3641
if settings.database_settings.uri is None:
3742
return None
@@ -43,7 +48,9 @@ async def get_database_engine(settings: Settings = Depends(get_settings)):
4348
)
4449

4550

46-
async def get_database_session(engine=Depends(get_database_engine)):
51+
async def get_database_session(
52+
engine: AsyncEngine = Depends(get_database_engine),
53+
) -> AsyncGenerator[Optional[AsyncSession]]:
4754
# Special case for single-user mode
4855
if engine is None:
4956
yield None

tiled/commandline/_serve.py

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def serve_directory(
3636
"this option selected."
3737
),
3838
),
39-
api_key: str = typer.Option(
39+
api_key: Optional[str] = typer.Option(
4040
None,
4141
"--api-key",
4242
help=(
@@ -55,7 +55,7 @@ def serve_directory(
5555
"may break user (client-side) code."
5656
),
5757
),
58-
ext: List[str] = typer.Option(
58+
ext: Optional[List[str]] = typer.Option(
5959
None,
6060
"--ext",
6161
help=(
@@ -64,7 +64,7 @@ def serve_directory(
6464
"extension."
6565
),
6666
),
67-
mimetype_detection_hook: str = typer.Option(
67+
mimetype_detection_hook: Optional[str] = typer.Option(
6868
None,
6969
"--mimetype-hook",
7070
help=(
@@ -73,15 +73,15 @@ def serve_directory(
7373
"Specify here as 'package.module:function'"
7474
),
7575
),
76-
adapters: List[str] = typer.Option(
76+
adapters: Optional[List[str]] = typer.Option(
7777
None,
7878
"--adapter",
7979
help=(
8080
"ADVANCED: Custom Tiled Adapter for reading a given format"
8181
"Specify here as 'mimetype=package.module:function'"
8282
),
8383
),
84-
walkers: List[str] = typer.Option(
84+
walkers: Optional[List[str]] = typer.Option(
8585
None,
8686
"--walker",
8787
help=(
@@ -138,7 +138,7 @@ def serve_directory(
138138
stamp_head(ALEMBIC_INI_TEMPLATE_PATH, ALEMBIC_DIR, database)
139139

140140
from ..catalog import from_uri as catalog_from_uri
141-
from ..server.app import build_app, print_admin_api_key_if_generated
141+
from ..server.app import build_app, print_server_info
142142

143143
server_settings = {}
144144
if keep_ext:
@@ -212,7 +212,7 @@ def serve_directory(
212212

213213
from ..client import from_uri as client_from_uri
214214

215-
print_admin_api_key_if_generated(web_app, host=host, port=port, force=generated)
215+
print_server_info(web_app, host=host, port=port, include_api_key=generated)
216216
log_config = _setup_log_config(log_config, log_timestamps)
217217
config = uvicorn.Config(web_app, host=host, port=port, log_config=log_config)
218218
server = uvicorn.Server(config)
@@ -287,16 +287,16 @@ async def serve_and_walk():
287287

288288

289289
def serve_catalog(
290-
database: str = typer.Argument(
290+
database: Optional[str] = typer.Argument(
291291
None, help="A filepath or database URI, e.g. 'catalog.db'"
292292
),
293-
read: List[str] = typer.Option(
293+
read: Optional[List[str]] = typer.Option(
294294
None,
295295
"--read",
296296
"-r",
297297
help="Locations that the server may read from",
298298
),
299-
write: List[str] = typer.Option(
299+
write: Optional[List[str]] = typer.Option(
300300
None,
301301
"--write",
302302
"-w",
@@ -321,7 +321,7 @@ def serve_catalog(
321321
"this option selected."
322322
),
323323
),
324-
api_key: str = typer.Option(
324+
api_key: Optional[str] = typer.Option(
325325
None,
326326
"--api-key",
327327
help=(
@@ -363,7 +363,7 @@ def serve_catalog(
363363

364364
from ..catalog import from_uri
365365
from ..catalog.utils import classify_writable_storage
366-
from ..server.app import build_app, print_admin_api_key_if_generated
366+
from ..server.app import build_app, print_server_info
367367

368368
parsed_database = urllib.parse.urlparse(database)
369369
if parsed_database.scheme in ("", "file"):
@@ -477,7 +477,9 @@ def serve_catalog(
477477
server_settings,
478478
scalable=scalable,
479479
)
480-
print_admin_api_key_if_generated(web_app, host=host, port=port)
480+
print_server_info(
481+
web_app, host=host, port=port, include_api_key=api_key is not None
482+
)
481483

482484
import uvicorn
483485

@@ -502,7 +504,7 @@ def serve_pyobject(
502504
"option selected."
503505
),
504506
),
505-
api_key: str = typer.Option(
507+
api_key: Optional[str] = typer.Option(
506508
None,
507509
"--api-key",
508510
help=(
@@ -534,7 +536,7 @@ def serve_pyobject(
534536
),
535537
):
536538
"Serve a Tree instance from a Python module."
537-
from ..server.app import build_app, print_admin_api_key_if_generated
539+
from ..server.app import build_app, print_server_info
538540
from ..utils import import_object
539541

540542
tree = import_object(object_path)
@@ -548,7 +550,7 @@ def serve_pyobject(
548550
server_settings,
549551
scalable=scalable,
550552
)
551-
print_admin_api_key_if_generated(web_app, host=host, port=port)
553+
print_server_info(web_app, host=host, port=port, include_api_key=api_key is None)
552554

553555
import uvicorn
554556

@@ -569,13 +571,13 @@ def serve_demo(
569571
port: int = typer.Option(8000, help="Bind to a socket with this port."),
570572
):
571573
"Start a public server with example data."
572-
from ..server.app import build_app, print_admin_api_key_if_generated
574+
from ..server.app import build_app, print_server_info
573575
from ..utils import import_object
574576

575577
EXAMPLE = "tiled.examples.generated:tree"
576578
tree = import_object(EXAMPLE)
577579
web_app = build_app(tree, {"allow_anonymous_access": True}, {})
578-
print_admin_api_key_if_generated(web_app, host=host, port=port)
580+
print_server_info(web_app, host=host, port=port, include_api_key=True)
579581

580582
import uvicorn
581583

@@ -584,7 +586,7 @@ def serve_demo(
584586

585587
@serve_app.command("config")
586588
def serve_config(
587-
config_path: Path = typer.Argument(
589+
config_path: Optional[Path] = typer.Argument(
588590
None,
589591
help=(
590592
"Path to a config file or directory of config files. "
@@ -601,23 +603,23 @@ def serve_config(
601603
"option selected."
602604
),
603605
),
604-
api_key: str = typer.Option(
606+
api_key: Optional[str] = typer.Option(
605607
None,
606608
"--api-key",
607609
help=(
608610
"Set the single-user API key. "
609611
"By default, a random key is generated at startup and printed."
610612
),
611613
),
612-
host: str = typer.Option(
614+
host: Optional[str] = typer.Option(
613615
None,
614616
help=(
615617
"Bind socket to this host. Use `--host 0.0.0.0` to make the application "
616618
"available on your local network. IPv6 addresses are supported, for "
617619
"example: --host `'::'`. Uses value in config by default."
618620
),
619621
),
620-
port: int = typer.Option(
622+
port: Optional[int] = typer.Option(
621623
None, help="Bind to a socket with this port. Uses value in config by default."
622624
),
623625
scalable: bool = typer.Option(
@@ -659,11 +661,7 @@ def serve_config(
659661

660662
# Delay this import so that we can fail faster if config-parsing fails above.
661663

662-
from ..server.app import (
663-
build_app_from_config,
664-
logger,
665-
print_admin_api_key_if_generated,
666-
)
664+
from ..server.app import build_app_from_config, logger, print_server_info
667665

668666
# Extract config for uvicorn.
669667
uvicorn_kwargs = parsed_config.pop("uvicorn", {})
@@ -684,8 +682,11 @@ def serve_config(
684682
web_app = build_app_from_config(
685683
parsed_config, source_filepath=config_path, scalable=scalable
686684
)
687-
print_admin_api_key_if_generated(
688-
web_app, host=uvicorn_kwargs["host"], port=uvicorn_kwargs["port"]
685+
print_server_info(
686+
web_app,
687+
host=uvicorn_kwargs["host"],
688+
port=uvicorn_kwargs["port"],
689+
include_api_key=api_key is None,
689690
)
690691

691692
# Likewise, delay this import.

tiled/server/app.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,9 @@ async def unhandled_exception_handler(
428428
)
429429
# And add this authentication_router itself to the app.
430430
app.include_router(authentication_router, prefix="/api/v1/auth")
431+
app.state.authenticated = True
432+
else:
433+
app.state.authenticated = False
431434

432435
@cache
433436
def override_get_authenticators():
@@ -450,8 +453,6 @@ def override_get_settings():
450453
]:
451454
if authentication.get(item) is not None:
452455
setattr(settings, item, authentication[item])
453-
if authentication.get("single_user_api_key") is not None:
454-
settings.single_user_api_key_generated = False
455456
for item in [
456457
"allow_origins",
457458
"response_bytesize_limit",
@@ -884,7 +885,7 @@ def app_factory():
884885
kwargs = construct_build_app_kwargs(parsed_config, source_filepath=config_path)
885886
web_app = build_app(**kwargs)
886887
uvicorn_config = parsed_config.get("uvicorn", {})
887-
print_admin_api_key_if_generated(
888+
print_server_info(
888889
web_app, host=uvicorn_config.get("host"), port=uvicorn_config.get("port")
889890
)
890891
return web_app
@@ -902,16 +903,14 @@ def __getattr__(name):
902903
raise AttributeError(name)
903904

904905

905-
def print_admin_api_key_if_generated(
906-
web_app: FastAPI, host: str, port: int, force: bool = False
906+
def print_server_info(
907+
web_app: FastAPI,
908+
host: str = "127.0.0.1",
909+
port: int = 8000,
910+
include_api_key: bool = False,
907911
):
908-
"Print message to stderr with API key if server-generated (or force=True)."
909-
host = host or "127.0.0.1"
910-
port = port or 8000
911-
settings = web_app.dependency_overrides.get(get_settings, get_settings)()
912-
authenticators = web_app.dependency_overrides.get(
913-
get_authenticators, get_authenticators
914-
)()
912+
settings = get_settings()
913+
915914
if settings.allow_anonymous_access:
916915
print(
917916
"""
@@ -921,7 +920,7 @@ def print_admin_api_key_if_generated(
921920
""",
922921
file=sys.stderr,
923922
)
924-
if (not authenticators) and (force or settings.single_user_api_key_generated):
923+
if not web_app.state.authenticated and include_api_key:
925924
print(
926925
f"""
927926
Navigate a web browser or connect a Tiled client to:

0 commit comments

Comments
 (0)