remove asyncclick
This commit is contained in:
		| @@ -1 +1 @@ | |||||||
| __version__ = "0.2.4" | __version__ = "0.2.5" | ||||||
|   | |||||||
| @@ -1,10 +1,12 @@ | |||||||
|  | import asyncio | ||||||
| import json | import json | ||||||
| import os | import os | ||||||
| import sys | import sys | ||||||
| from configparser import ConfigParser | from configparser import ConfigParser | ||||||
|  | from functools import wraps | ||||||
|  |  | ||||||
| import asyncclick as click | import click | ||||||
| from asyncclick import Context, UsageError | from click import Context, UsageError | ||||||
| from tortoise import Tortoise, generate_schema_for_client | from tortoise import Tortoise, generate_schema_for_client | ||||||
| from tortoise.exceptions import OperationalError | from tortoise.exceptions import OperationalError | ||||||
| from tortoise.transactions import in_transaction | from tortoise.transactions import in_transaction | ||||||
| @@ -20,6 +22,14 @@ from .models import Aerich | |||||||
| parser = ConfigParser() | parser = ConfigParser() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def coro(f): | ||||||
|  |     @wraps(f) | ||||||
|  |     def wrapper(*args, **kwargs): | ||||||
|  |         return asyncio.run(f(*args, **kwargs)) | ||||||
|  |  | ||||||
|  |     return wrapper | ||||||
|  |  | ||||||
|  |  | ||||||
| @click.group(context_settings={"help_option_names": ["-h", "--help"]}) | @click.group(context_settings={"help_option_names": ["-h", "--help"]}) | ||||||
| @click.version_option(__version__, "-V", "--version") | @click.version_option(__version__, "-V", "--version") | ||||||
| @click.option( | @click.option( | ||||||
| @@ -34,6 +44,7 @@ parser = ConfigParser() | |||||||
|     help="Name of section in .ini file to use for aerich config.", |     help="Name of section in .ini file to use for aerich config.", | ||||||
| ) | ) | ||||||
| @click.pass_context | @click.pass_context | ||||||
|  | @coro | ||||||
| async def cli(ctx: Context, config, app, name): | async def cli(ctx: Context, config, app, name): | ||||||
|     ctx.ensure_object(dict) |     ctx.ensure_object(dict) | ||||||
|     ctx.obj["config_file"] = config |     ctx.obj["config_file"] = config | ||||||
| @@ -63,6 +74,7 @@ async def cli(ctx: Context, config, app, name): | |||||||
| @cli.command(help="Generate migrate changes file.") | @cli.command(help="Generate migrate changes file.") | ||||||
| @click.option("--name", default="update", show_default=True, help="Migrate name.") | @click.option("--name", default="update", show_default=True, help="Migrate name.") | ||||||
| @click.pass_context | @click.pass_context | ||||||
|  | @coro | ||||||
| async def migrate(ctx: Context, name): | async def migrate(ctx: Context, name): | ||||||
|     config = ctx.obj["config"] |     config = ctx.obj["config"] | ||||||
|     location = ctx.obj["location"] |     location = ctx.obj["location"] | ||||||
| @@ -76,6 +88,7 @@ async def migrate(ctx: Context, name): | |||||||
|  |  | ||||||
| @cli.command(help="Upgrade to latest version.") | @cli.command(help="Upgrade to latest version.") | ||||||
| @click.pass_context | @click.pass_context | ||||||
|  | @coro | ||||||
| async def upgrade(ctx: Context): | async def upgrade(ctx: Context): | ||||||
|     config = ctx.obj["config"] |     config = ctx.obj["config"] | ||||||
|     app = ctx.obj["app"] |     app = ctx.obj["app"] | ||||||
| @@ -102,6 +115,7 @@ async def upgrade(ctx: Context): | |||||||
|  |  | ||||||
| @cli.command(help="Downgrade to previous version.") | @cli.command(help="Downgrade to previous version.") | ||||||
| @click.pass_context | @click.pass_context | ||||||
|  | @coro | ||||||
| async def downgrade(ctx: Context): | async def downgrade(ctx: Context): | ||||||
|     app = ctx.obj["app"] |     app = ctx.obj["app"] | ||||||
|     config = ctx.obj["config"] |     config = ctx.obj["config"] | ||||||
| @@ -124,6 +138,7 @@ async def downgrade(ctx: Context): | |||||||
|  |  | ||||||
| @cli.command(help="Show current available heads in migrate location.") | @cli.command(help="Show current available heads in migrate location.") | ||||||
| @click.pass_context | @click.pass_context | ||||||
|  | @coro | ||||||
| async def heads(ctx: Context): | async def heads(ctx: Context): | ||||||
|     app = ctx.obj["app"] |     app = ctx.obj["app"] | ||||||
|     versions = Migrate.get_all_version_files() |     versions = Migrate.get_all_version_files() | ||||||
| @@ -138,6 +153,7 @@ async def heads(ctx: Context): | |||||||
|  |  | ||||||
| @cli.command(help="List all migrate items.") | @cli.command(help="List all migrate items.") | ||||||
| @click.pass_context | @click.pass_context | ||||||
|  | @coro | ||||||
| async def history(ctx: Context): | async def history(ctx: Context): | ||||||
|     versions = Migrate.get_all_version_files() |     versions = Migrate.get_all_version_files() | ||||||
|     for version in versions: |     for version in versions: | ||||||
| @@ -157,6 +173,7 @@ async def history(ctx: Context): | |||||||
|     "--location", default="./migrations", show_default=True, help="Migrate store location." |     "--location", default="./migrations", show_default=True, help="Migrate store location." | ||||||
| ) | ) | ||||||
| @click.pass_context | @click.pass_context | ||||||
|  | @coro | ||||||
| async def init( | async def init( | ||||||
|     ctx: Context, tortoise_orm, location, |     ctx: Context, tortoise_orm, location, | ||||||
| ): | ): | ||||||
| @@ -188,6 +205,7 @@ async def init( | |||||||
|     show_default=True, |     show_default=True, | ||||||
| ) | ) | ||||||
| @click.pass_context | @click.pass_context | ||||||
|  | @coro | ||||||
| async def init_db(ctx: Context, safe): | async def init_db(ctx: Context, safe): | ||||||
|     config = ctx.obj["config"] |     config = ctx.obj["config"] | ||||||
|     location = ctx.obj["location"] |     location = ctx.obj["location"] | ||||||
| @@ -220,4 +238,4 @@ async def init_db(ctx: Context, safe): | |||||||
|  |  | ||||||
| def main(): | def main(): | ||||||
|     sys.path.insert(0, ".") |     sys.path.insert(0, ".") | ||||||
|     cli(_anyio_backend="asyncio") |     cli() | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import importlib | import importlib | ||||||
|  |  | ||||||
| from asyncclick import BadOptionUsage, Context | from click import BadOptionUsage, Context | ||||||
| from tortoise import BaseDBAsyncClient, Tortoise | from tortoise import BaseDBAsyncClient, Tortoise | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										76
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										76
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							| @@ -23,28 +23,6 @@ version = "0.15.0" | |||||||
| [package.dependencies] | [package.dependencies] | ||||||
| typing_extensions = "*" | typing_extensions = "*" | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| category = "main" |  | ||||||
| description = "High level compatibility layer for multiple asynchronous event loop implementations" |  | ||||||
| name = "anyio" |  | ||||||
| optional = false |  | ||||||
| python-versions = ">=3.6.2" |  | ||||||
| version = "2.0.0" |  | ||||||
|  |  | ||||||
| [package.dependencies] |  | ||||||
| idna = ">=2.8" |  | ||||||
| sniffio = ">=1.1" |  | ||||||
|  |  | ||||||
| [package.dependencies.typing-extensions] |  | ||||||
| python = "<3.8" |  | ||||||
| version = "*" |  | ||||||
|  |  | ||||||
| [package.extras] |  | ||||||
| curio = ["curio (>=1.4)"] |  | ||||||
| doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] |  | ||||||
| test = ["coverage (>=4.5)", "hypothesis (>=4.0)", "pytest (>=4.3)", "trustme", "uvloop"] |  | ||||||
| trio = ["trio (>=0.16)"] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| category = "dev" | category = "dev" | ||||||
| description = "apipkg: namespace control and lazy-import mechanism" | description = "apipkg: namespace control and lazy-import mechanism" | ||||||
| @@ -61,21 +39,6 @@ optional = false | |||||||
| python-versions = "*" | python-versions = "*" | ||||||
| version = "1.4.4" | version = "1.4.4" | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| category = "main" |  | ||||||
| description = "A simple anyio-compatible fork of Click, for powerful command line utilities." |  | ||||||
| name = "asyncclick" |  | ||||||
| optional = false |  | ||||||
| python-versions = ">=3.6" |  | ||||||
| version = "7.1.2.1" |  | ||||||
|  |  | ||||||
| [package.dependencies] |  | ||||||
| anyio = ">=2" |  | ||||||
|  |  | ||||||
| [package.extras] |  | ||||||
| dev = ["coverage", "pytest-runner", "pytest-trio", "pytest (>=3)", "sphinx", "tox"] |  | ||||||
| docs = ["sphinx"] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| category = "main" | category = "main" | ||||||
| description = "An asyncio PostgreSQL driver" | description = "An asyncio PostgreSQL driver" | ||||||
| @@ -159,7 +122,7 @@ version = "1.14.3" | |||||||
| pycparser = "*" | pycparser = "*" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| category = "dev" | category = "main" | ||||||
| description = "Composable command line interface toolkit" | description = "Composable command line interface toolkit" | ||||||
| name = "click" | name = "click" | ||||||
| optional = false | optional = false | ||||||
| @@ -247,14 +210,6 @@ version = "3.1.8" | |||||||
| [package.dependencies] | [package.dependencies] | ||||||
| gitdb = ">=4.0.1,<5" | gitdb = ">=4.0.1,<5" | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| category = "main" |  | ||||||
| description = "Internationalized Domain Names in Applications (IDNA)" |  | ||||||
| name = "idna" |  | ||||||
| optional = false |  | ||||||
| python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" |  | ||||||
| version = "2.10" |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| category = "dev" | category = "dev" | ||||||
| description = "Read metadata from Python packages" | description = "Read metadata from Python packages" | ||||||
| @@ -430,7 +385,7 @@ description = "A SQL query builder API for Python" | |||||||
| name = "pypika" | name = "pypika" | ||||||
| optional = false | optional = false | ||||||
| python-versions = "*" | python-versions = "*" | ||||||
| version = "0.39.1" | version = "0.40.0" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| category = "dev" | category = "dev" | ||||||
| @@ -548,14 +503,6 @@ optional = false | |||||||
| python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" | ||||||
| version = "3.0.4" | version = "3.0.4" | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| category = "main" |  | ||||||
| description = "Sniff out which async library your code is running under" |  | ||||||
| name = "sniffio" |  | ||||||
| optional = false |  | ||||||
| python-versions = ">=3.5" |  | ||||||
| version = "1.1.0" |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| category = "dev" | category = "dev" | ||||||
| description = "Manage dynamic plugins for Python applications" | description = "Manage dynamic plugins for Python applications" | ||||||
| @@ -629,7 +576,7 @@ testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pyt | |||||||
| dbdrivers = ["aiomysql", "asyncpg"] | dbdrivers = ["aiomysql", "asyncpg"] | ||||||
|  |  | ||||||
| [metadata] | [metadata] | ||||||
| content-hash = "06f00778f783c4ad5b174a9c9ee80f4f0e38db9da9ff1012f09c7d306eaa0975" | content-hash = "43fe9c0036f4d55d38f82c263887d8a7d9a35a597e02036b70a631955ff73149" | ||||||
| lock-version = "1.0" | lock-version = "1.0" | ||||||
| python-versions = "^3.7" | python-versions = "^3.7" | ||||||
|  |  | ||||||
| @@ -642,10 +589,6 @@ aiosqlite = [ | |||||||
|     {file = "aiosqlite-0.15.0-py3-none-any.whl", hash = "sha256:19b984b6702aed9f1c85c023f37296954547fc4030dae8e9d027b2a930bed78b"}, |     {file = "aiosqlite-0.15.0-py3-none-any.whl", hash = "sha256:19b984b6702aed9f1c85c023f37296954547fc4030dae8e9d027b2a930bed78b"}, | ||||||
|     {file = "aiosqlite-0.15.0.tar.gz", hash = "sha256:a2884793f4dc8f2798d90e1dfecb2b56a6d479cf039f7ec52356a7fd5f3bdc57"}, |     {file = "aiosqlite-0.15.0.tar.gz", hash = "sha256:a2884793f4dc8f2798d90e1dfecb2b56a6d479cf039f7ec52356a7fd5f3bdc57"}, | ||||||
| ] | ] | ||||||
| anyio = [ |  | ||||||
|     {file = "anyio-2.0.0-py3-none-any.whl", hash = "sha256:0b8375c8fc665236cb4d143ea13e849eb9e074d727b1b5c27d88aba44ca8c547"}, |  | ||||||
|     {file = "anyio-2.0.0.tar.gz", hash = "sha256:ceca4669ffa3f02bf20ef3d6c2a0c323b16cdc71d1ce0b0bc03c6f1f36054826"}, |  | ||||||
| ] |  | ||||||
| apipkg = [ | apipkg = [ | ||||||
|     {file = "apipkg-1.5-py2.py3-none-any.whl", hash = "sha256:58587dd4dc3daefad0487f6d9ae32b4542b185e1c36db6993290e7c41ca2b47c"}, |     {file = "apipkg-1.5-py2.py3-none-any.whl", hash = "sha256:58587dd4dc3daefad0487f6d9ae32b4542b185e1c36db6993290e7c41ca2b47c"}, | ||||||
|     {file = "apipkg-1.5.tar.gz", hash = "sha256:37228cda29411948b422fae072f57e31d3396d2ee1c9783775980ee9c9990af6"}, |     {file = "apipkg-1.5.tar.gz", hash = "sha256:37228cda29411948b422fae072f57e31d3396d2ee1c9783775980ee9c9990af6"}, | ||||||
| @@ -654,9 +597,6 @@ appdirs = [ | |||||||
|     {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, |     {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, | ||||||
|     {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, |     {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, | ||||||
| ] | ] | ||||||
| asyncclick = [ |  | ||||||
|     {file = "asyncclick-7.1.2.1.tar.gz", hash = "sha256:fe9fd8c44a6ae396e54471bd5f209838d46124e019ae701dd71a9c898928483b"}, |  | ||||||
| ] |  | ||||||
| asyncpg = [ | asyncpg = [ | ||||||
|     {file = "asyncpg-0.21.0-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:09badce47a4645cfe523cc8a182bd047d5d62af0caaea77935e6a3c9e77dc364"}, |     {file = "asyncpg-0.21.0-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:09badce47a4645cfe523cc8a182bd047d5d62af0caaea77935e6a3c9e77dc364"}, | ||||||
|     {file = "asyncpg-0.21.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b7807bfedd24dd15cfb2c17c60977ce01410615ecc285268b5144a944ec97ff"}, |     {file = "asyncpg-0.21.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b7807bfedd24dd15cfb2c17c60977ce01410615ecc285268b5144a944ec97ff"}, | ||||||
| @@ -786,10 +726,6 @@ gitpython = [ | |||||||
|     {file = "GitPython-3.1.8-py3-none-any.whl", hash = "sha256:1858f4fd089abe92ae465f01d5aaaf55e937eca565fb2c1fce35a51b5f85c910"}, |     {file = "GitPython-3.1.8-py3-none-any.whl", hash = "sha256:1858f4fd089abe92ae465f01d5aaaf55e937eca565fb2c1fce35a51b5f85c910"}, | ||||||
|     {file = "GitPython-3.1.8.tar.gz", hash = "sha256:080bf8e2cf1a2b907634761c2eaefbe83b69930c94c66ad11b65a8252959f912"}, |     {file = "GitPython-3.1.8.tar.gz", hash = "sha256:080bf8e2cf1a2b907634761c2eaefbe83b69930c94c66ad11b65a8252959f912"}, | ||||||
| ] | ] | ||||||
| idna = [ |  | ||||||
|     {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, |  | ||||||
|     {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, |  | ||||||
| ] |  | ||||||
| importlib-metadata = [ | importlib-metadata = [ | ||||||
|     {file = "importlib_metadata-2.0.0-py2.py3-none-any.whl", hash = "sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3"}, |     {file = "importlib_metadata-2.0.0-py2.py3-none-any.whl", hash = "sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3"}, | ||||||
|     {file = "importlib_metadata-2.0.0.tar.gz", hash = "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da"}, |     {file = "importlib_metadata-2.0.0.tar.gz", hash = "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da"}, | ||||||
| @@ -875,7 +811,7 @@ pyparsing = [ | |||||||
|     {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, |     {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, | ||||||
| ] | ] | ||||||
| pypika = [ | pypika = [ | ||||||
|     {file = "PyPika-0.39.1.tar.gz", hash = "sha256:f6c715348cb6fa0042fe5b69f40cbb19c36ee3ff68414ece8660465c56523012"}, |     {file = "PyPika-0.40.0.tar.gz", hash = "sha256:659d307f7e531b66813619cbce08ecb97eeb302feabbd816ae8844b99496298b"}, | ||||||
| ] | ] | ||||||
| pytest = [ | pytest = [ | ||||||
|     {file = "pytest-6.0.2-py3-none-any.whl", hash = "sha256:0e37f61339c4578776e090c3b8f6b16ce4db333889d65d0efb305243ec544b40"}, |     {file = "pytest-6.0.2-py3-none-any.whl", hash = "sha256:0e37f61339c4578776e090c3b8f6b16ce4db333889d65d0efb305243ec544b40"}, | ||||||
| @@ -940,10 +876,6 @@ smmap = [ | |||||||
|     {file = "smmap-3.0.4-py2.py3-none-any.whl", hash = "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4"}, |     {file = "smmap-3.0.4-py2.py3-none-any.whl", hash = "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4"}, | ||||||
|     {file = "smmap-3.0.4.tar.gz", hash = "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"}, |     {file = "smmap-3.0.4.tar.gz", hash = "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"}, | ||||||
| ] | ] | ||||||
| sniffio = [ |  | ||||||
|     {file = "sniffio-1.1.0-py3-none-any.whl", hash = "sha256:20ed6d5b46f8ae136d00b9dcb807615d83ed82ceea6b2058cecb696765246da5"}, |  | ||||||
|     {file = "sniffio-1.1.0.tar.gz", hash = "sha256:8e3810100f69fe0edd463d02ad407112542a11ffdc29f67db2bf3771afb87a21"}, |  | ||||||
| ] |  | ||||||
| stevedore = [ | stevedore = [ | ||||||
|     {file = "stevedore-3.2.2-py3-none-any.whl", hash = "sha256:5e1ab03eaae06ef6ce23859402de785f08d97780ed774948ef16c4652c41bc62"}, |     {file = "stevedore-3.2.2-py3-none-any.whl", hash = "sha256:5e1ab03eaae06ef6ce23859402de785f08d97780ed774948ef16c4652c41bc62"}, | ||||||
|     {file = "stevedore-3.2.2.tar.gz", hash = "sha256:f845868b3a3a77a2489d226568abe7328b5c2d4f6a011cc759dfa99144a521f0"}, |     {file = "stevedore-3.2.2.tar.gz", hash = "sha256:f845868b3a3a77a2489d226568abe7328b5c2d4f6a011cc759dfa99144a521f0"}, | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| [tool.poetry] | [tool.poetry] | ||||||
| name = "aerich" | name = "aerich" | ||||||
| version = "0.2.4" | version = "0.2.5" | ||||||
| description = "A database migrations tool for Tortoise ORM." | description = "A database migrations tool for Tortoise ORM." | ||||||
| authors = ["long2ice <long2ice@gmail.com>"] | authors = ["long2ice <long2ice@gmail.com>"] | ||||||
| license = "Apache-2.0" | license = "Apache-2.0" | ||||||
| @@ -17,7 +17,7 @@ include = ["CHANGELOG.md", "LICENSE", "README.md"] | |||||||
| [tool.poetry.dependencies] | [tool.poetry.dependencies] | ||||||
| python = "^3.7" | python = "^3.7" | ||||||
| tortoise-orm = "*" | tortoise-orm = "*" | ||||||
| asyncclick = "*" | click = "*" | ||||||
| pydantic = "*" | pydantic = "*" | ||||||
| aiomysql = {version = "*", optional = true} | aiomysql = {version = "*", optional = true} | ||||||
| asyncpg = {version = "*", optional = true} | asyncpg = {version = "*", optional = true} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user