Skip to content

Commit 917a110

Browse files
committed
Add cli
1 parent 2105634 commit 917a110

File tree

7 files changed

+284
-34
lines changed

7 files changed

+284
-34
lines changed

blocks/sport.yaml

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
html: false
2+
upstream: http://trading-api.prod.svc
3+
tags:
4+
- trading
5+
- api
6+
routes:
7+
- name: acme
8+
protocols:
9+
- http
10+
- https
11+
paths:
12+
- /.well-known/acme-challenge
13+
tags:
14+
- public
15+
- acme
16+
- status
17+
- name: public
18+
protocols:
19+
- https
20+
paths:
21+
- /v1/
22+
- /v1/countries
23+
- /v1/docs
24+
- /v1/spec
25+
- /v1/status
26+
- /v1/_tl
27+
strip_path: false
28+
plugins:
29+
- name: cors
30+
config:
31+
origins:
32+
- "*"
33+
methods:
34+
- GET
35+
tags:
36+
- public
37+
- name: private
38+
protocols:
39+
- https
40+
paths:
41+
- /v1/accounts
42+
- /v1/alerts
43+
- /v1/betting
44+
- /v1/entities
45+
- /v1/events
46+
- /v1/gateways
47+
- /v1/instruments
48+
- /v1/leagues
49+
- /v1/names
50+
- /v1/orders
51+
- /v1/risk
52+
- /v1/strategies
53+
- /v1/teams
54+
- /v1/tasks
55+
- /v1/ohlc
56+
strip_path: false
57+
plugins:
58+
- name: key-auth
59+
config:
60+
key_names:
61+
- x-fluidily-api-key
62+
anonymous: anonymous
63+
- name: jwt
64+
config:
65+
claims_to_verify:
66+
- exp
67+
run_on_preflight: false
68+
anonymous: anonymous
69+
- name: acl
70+
config:
71+
allow:
72+
- service
73+
- service-fluidily
74+
- users-fluidily
75+
- name: request-termination
76+
consumer:
77+
username: anonymous
78+
config:
79+
status_code: 403
80+
message: "requires authentication"
81+
- name: cors
82+
config:
83+
origins:
84+
- "*"
85+
headers:
86+
- Authorization
87+
- Content-Type
88+
- User-Agent
89+
- x-fluidily-org-id
90+
exposed_headers:
91+
- Link
92+
- X-Total-Count
93+
- X-RateLimit-Limit-minute
94+
- X-RateLimit-Remaining-minute

metablock/cli.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import asyncio
2+
import os
3+
from pathlib import Path
4+
from typing import cast
5+
6+
import click
7+
import yaml
8+
from metablock import Metablock, Space
9+
10+
METABLOCK_SPACE = os.environ.get("METABLOCK_SPACE", "")
11+
METABLOCK_API_TOKEN = os.environ.get("METABLOCK_API_TOKEN", "")
12+
13+
14+
def manifest(file_path: Path) -> dict:
15+
return yaml.safe_load(file_path.read_text())
16+
17+
18+
@click.group()
19+
def cli() -> None:
20+
pass
21+
22+
23+
@cli.command()
24+
@click.argument("path", type=click.Path(exists=True))
25+
@click.option("--space", "space_name", help="Space name", default=METABLOCK_SPACE)
26+
@click.option("--token", help="metablock API token", default=METABLOCK_API_TOKEN)
27+
def apply(path: str, space_name: str, token: str) -> None:
28+
"""Apply metablock manifest"""
29+
asyncio.get_event_loop().run_until_complete(_apply(path, space_name, token))
30+
31+
32+
async def _apply(path: str, space_name: str, token: str) -> None:
33+
if not token:
34+
click.echo("metablock API token is required", err=True)
35+
raise click.Abort()
36+
if not space_name:
37+
click.echo("metablock space is required", err=True)
38+
raise click.Abort()
39+
p = Path(path)
40+
blocks = []
41+
for file_path in p.glob("*.yaml"):
42+
name = file_path.name.split(".")[0]
43+
blocks.append((name, manifest(file_path)))
44+
async with Metablock(auth_key=token) as mb:
45+
space: Space = cast(Space, await mb.spaces.get(space_name))
46+
svc = await space.blocks.get_list()
47+
click.echo(f"space {space.name} has {len(svc)} blocks")
48+
by_name = {s["name"]: s for s in svc}
49+
for name, config in blocks:
50+
block = by_name.get(name)
51+
if block:
52+
# update
53+
click.echo(f"update block {name}")
54+
await mb.services.update(block.id, **config)
55+
else:
56+
# create
57+
click.echo(f"create new block {name}")
58+
await space.services.create(name=name, **config)
59+
60+
61+
if __name__ == "__main__":
62+
cli()

poetry.lock

Lines changed: 61 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ readme = "readme.md"
99
[tool.poetry.dependencies]
1010
python = ">=3.10,<3.12"
1111
aiohttp = "^3.8.3"
12+
click = "^8.1.3"
13+
pyyaml = "^6.0"
1214

1315
[tool.poetry.group.dev.dependencies]
1416
pytest-asyncio = "^0.21.0"
@@ -20,7 +22,11 @@ isort = "^5.11.3"
2022
flake8 = "^6.0.0"
2123
flake8-builtins = "^2.0.1"
2224
python-dotenv = "^1.0.0"
25+
types-pyyaml = "^6.0.12.10"
2326

2427
[build-system]
2528
requires = ["poetry-core>=1.0.0"]
2629
build-backend = "poetry.core.masonry.api"
30+
31+
[tool.poetry.scripts]
32+
metablock = "metablock.cli:main"

tests/blocks/test.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
html: false
2+
upstream: http://metablock.prod.svc
3+
tags:
4+
- test
5+
routes:
6+
- name: main
7+
protocols:
8+
- http
9+
- https
10+
paths:
11+
- /
12+
tags:
13+
- public

tests/test_cli.py

Lines changed: 12 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,16 @@
1-
import pytest
2-
from metablock import Metablock, MetablockResponseError
1+
from click.testing import CliRunner
2+
from metablock.cli import cli
33

44

5-
def test_cli(cli: Metablock):
6-
assert cli.url == "https://api.metablock.io/v1"
7-
assert str(cli) == cli.url
5+
def test_cli_apply_path_error():
6+
runner = CliRunner()
7+
result = runner.invoke(cli, ["apply", "foo"])
8+
assert result.exit_code == 2
9+
assert "Invalid value for 'PATH': Path 'foo' does not exist" in result.output
810

911

10-
async def test_user(cli: Metablock):
11-
user = await cli.get_user()
12-
assert user.id
13-
orgs = await user.orgs()
14-
assert orgs
15-
16-
17-
async def test_user_403(cli: Metablock, invalid_headers: dict):
18-
with pytest.raises(MetablockResponseError) as exc:
19-
await cli.get_user(headers=invalid_headers)
20-
assert exc.value.status == 403
21-
22-
23-
async def test_orgs_403(cli: Metablock, invalid_headers: dict):
24-
user = await cli.get_user()
25-
with pytest.raises(MetablockResponseError) as exc:
26-
await user.orgs(headers=invalid_headers)
27-
assert exc.value.status == 403
28-
29-
30-
async def test_space(cli: Metablock):
31-
space = await cli.get_space()
32-
assert space["name"] == "metablock"
33-
34-
35-
async def test_spec(cli: Metablock):
36-
spec = await cli.spec()
37-
assert spec
12+
def test_cli_apply_no_space():
13+
runner = CliRunner()
14+
result = runner.invoke(cli, ["apply", "tests/blocks"])
15+
assert result.exit_code == 1
16+
assert result.output.startswith("metablock space is required")

tests/test_client.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import pytest
2+
from metablock import Metablock, MetablockResponseError
3+
4+
5+
def test_cli(cli: Metablock):
6+
assert str(cli) == cli.url
7+
8+
9+
async def test_user(cli: Metablock):
10+
user = await cli.get_user()
11+
assert user.id
12+
orgs = await user.orgs()
13+
assert orgs
14+
15+
16+
async def test_user_403(cli: Metablock, invalid_headers: dict):
17+
with pytest.raises(MetablockResponseError) as exc:
18+
await cli.get_user(headers=invalid_headers)
19+
assert exc.value.status == 403
20+
21+
22+
async def test_orgs_403(cli: Metablock, invalid_headers: dict):
23+
user = await cli.get_user()
24+
with pytest.raises(MetablockResponseError) as exc:
25+
await user.orgs(headers=invalid_headers)
26+
assert exc.value.status == 403
27+
28+
29+
async def test_space(cli: Metablock):
30+
space = await cli.get_space()
31+
assert space["name"] == "metablock"
32+
33+
34+
async def test_spec(cli: Metablock):
35+
spec = await cli.spec()
36+
assert spec

0 commit comments

Comments
 (0)