Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP cli tool #44

Open
wants to merge 45 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
aa5ae3f
WIP cli
tbarbugli Jun 24, 2024
27db1ba
wip reflection based generated commands
sachaarbonel Jun 25, 2024
4f3e0b6
remove redudant click.echo in favour of print_result
sachaarbonel Jun 25, 2024
7e82ab0
Merge branch 'main' into cli
sachaarbonel Jun 25, 2024
79c073f
ruff fix
sachaarbonel Jun 25, 2024
ca3b403
fix test setup
sachaarbonel Jun 25, 2024
3d9d324
ruff fix
sachaarbonel Jun 25, 2024
853defa
handle --data
sachaarbonel Jun 25, 2024
a435f97
review: add better names
sachaarbonel Jun 25, 2024
a09ddce
more unit tests
sachaarbonel Jun 25, 2024
358c13e
test get or create
sachaarbonel Jul 3, 2024
cdb0115
more tests
sachaarbonel Jul 3, 2024
b61ec3c
docstrings
sachaarbonel Jul 3, 2024
eeb5095
more docstrings
sachaarbonel Jul 3, 2024
57170a4
dash - instead of underscore _
sachaarbonel Jul 3, 2024
809a218
more call commands
sachaarbonel Jul 3, 2024
543cfc9
test_cli_create_call_with_members
sachaarbonel Jul 3, 2024
33f80ff
mock_setup refactor to make tests more readable
sachaarbonel Jul 3, 2024
12f92e6
more tests
sachaarbonel Jul 3, 2024
be9adf1
tests tests tests
sachaarbonel Jul 3, 2024
8155403
ruff fix
sachaarbonel Jul 3, 2024
e1f5ad9
lint
sachaarbonel Jul 3, 2024
e25d9b3
rtmp input
tbarbugli Jul 3, 2024
159e42a
Merge branch 'cli' of github.com:GetStream/stream-py into cli
tbarbugli Jul 3, 2024
683314c
fix send-call-event cli
sachaarbonel Jul 4, 2024
9fd9e6a
makefile wip
sachaarbonel Jul 4, 2024
85219e8
makefile
sachaarbonel Jul 4, 2024
4062bdc
Dockerfile + makefile commands
sachaarbonel Jul 4, 2024
c296584
update readme
sachaarbonel Jul 4, 2024
7cab37a
rename to stream-cli
sachaarbonel Jul 5, 2024
caa764f
fix make command
sachaarbonel Jul 5, 2024
326f8ca
remove this log
sachaarbonel Jul 5, 2024
f45013b
completion + optional python-dotenv
sachaarbonel Jul 5, 2024
dfd00a9
poetry update
sachaarbonel Jul 5, 2024
cfdd3f7
update readme
sachaarbonel Jul 5, 2024
f6e049f
revert py dotenv change to prepare for config
sachaarbonel Jul 5, 2024
1c7f41e
config
sachaarbonel Jul 5, 2024
b6e7e2e
fix configure + completion install command
sachaarbonel Jul 5, 2024
dc2ad0d
improve instructions
sachaarbonel Jul 5, 2024
a7ba61a
stream-cli instructions
sachaarbonel Jul 5, 2024
3e1cd13
bring back support for env variables
sachaarbonel Jul 5, 2024
8254657
fix lock file
sachaarbonel Jul 5, 2024
2eea290
fix lints
sachaarbonel Jul 8, 2024
b2775c2
ending calls
tbarbugli Jul 9, 2024
0eaf9aa
Merge branch 'main' into cli
tbarbugli Sep 4, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ pyvenv.cfg
bin/*
lib/*
shell.nix
pyrightconfig.json
52 changes: 52 additions & 0 deletions getstream/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import click
from dotenv import load_dotenv
from typing import Optional
from getstream import Stream
from getstream.cli.utils import pass_client
from getstream.cli.video import video
from getstream.stream import BASE_URL


@click.group()
@click.option("--api-key")
@click.option("--api-secret")
@click.option("--base-url", default=BASE_URL, show_default=True)
@click.option("--timeout", default=3.0, show_default=True)
@click.pass_context
def cli(ctx: click.Context, api_key: str, api_secret: str, base_url: str, timeout=3.0):
ctx.ensure_object(dict)
ctx.obj["client"] = Stream(
api_key=api_key, api_secret=api_secret, timeout=timeout, base_url=base_url
)


@click.command()
@click.option("--user-id", required=True)
@click.option("--call-cid", multiple=True, default=None)
@click.option("--role", default=None)
@click.option("--exp-seconds", type=int, default=None)
@pass_client
def create_token(
client: Stream, user_id: str, call_cid=None, role: Optional[str] = None, exp_seconds=None
):
if call_cid is not None and len(call_cid) > 0:
print(
client.create_call_token(
user_id=user_id, call_cids=call_cid, role=role, expiration=exp_seconds
)
)
else:
print(client.create_call_token(user_id=user_id))


cli.add_command(create_token)
cli.add_command(video)
#cli.add_command(chat)

def main():
load_dotenv()
cli(auto_envvar_prefix="STREAM", obj={})


if __name__ == "__main__":
main()
4 changes: 4 additions & 0 deletions getstream/cli/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import main

if __name__ == "__main__":
main()
35 changes: 35 additions & 0 deletions getstream/cli/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from functools import update_wrapper
import click

import json

def pass_client(f):
"""
Decorator that adds the Stream client to the decorated function, with this decorator you can write click commands like this

@click.command()
@click.option("--some-option")
@pass_client
def do_something(client: Stream, some_option):
pass

"""

@click.pass_context
def new_func(ctx, *args, **kwargs):
return ctx.invoke(f, ctx.obj["client"], *args, **kwargs)

return update_wrapper(new_func, f)

def json_option(option_name):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does this do? lets add a docstring with example

def decorator(f):
def callback(ctx, param, value):
if value is not None:
try:
return json.loads(value)
except json.JSONDecodeError:
raise click.BadParameter("Invalid JSON")
return value

return click.option(option_name, callback=callback)(f)
return decorator
144 changes: 144 additions & 0 deletions getstream/cli/video.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import click
import inspect
from getstream.models import CallRequest
from getstream import Stream
from getstream.stream_response import StreamResponse
import uuid
from getstream.video.call import Call
from getstream.video.client import VideoClient
from getstream.cli.utils import pass_client, json_option
import json

def print_result(result):
if isinstance(result, StreamResponse):
# TODO: verbose mode
# click.echo(f"Status Code: {result.status_code()}")
# click.echo("Headers:")
# for key, value in result.headers().items():
# click.echo(f" {key}: {value}")
click.echo("Data:")
click.echo(json.dumps(result.data.to_dict(), indent=2, default=str))
# rate_limits = result.rate_limit()
# if rate_limits:
# click.echo("Rate Limits:")
# click.echo(f" Limit: {rate_limits.limit}")
# click.echo(f" Remaining: {rate_limits.remaining}")
# click.echo(f" Reset: {rate_limits.reset}")
else:
click.echo(json.dumps(result, indent=2, default=str))

def create_call_command(name, method):
@click.command(name=name)
@click.option('--call-type', required=True, help='The type of the call')
@click.option('--call-id', required=True, help='The ID of the call')
@pass_client
def cmd(client, call_type, call_id, **kwargs):
call = client.video.call(call_type, call_id)
result = getattr(call, name)(**kwargs)
print_result(result)

sig = inspect.signature(method)
for param_name, param in sig.parameters.items():
if param_name in ['self', 'call_type', 'call_id']:
continue
add_option(cmd, param_name, param)

return cmd

def create_video_command(name, method):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can probably be moved to shared utils and called create_command_from_method

@click.command(name=name)
@pass_client
def cmd(client, **kwargs):
result = getattr(client.video, name)(**kwargs)
print_result(result)

sig = inspect.signature(method)
for param_name, param in sig.parameters.items():
if param_name == 'self':
continue
add_option(cmd, param_name, param)

return cmd

def add_option(cmd, param_name, param):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add_option_from_arg is bit easier to understand

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about optional vs not optional?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

highly suggested to write a bunch of unit tests for this function (fastest way to make it 100%)

if param.annotation == str:
cmd = click.option(f'--{param_name}', type=str)(cmd)
elif param.annotation == int:
cmd = click.option(f'--{param_name}', type=int)(cmd)
elif param.annotation == bool:
cmd = click.option(f'--{param_name}', is_flag=True)(cmd)
elif param.annotation == list:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is probably not good enough: a list of strings or a list of numbers can be handled with click options multiple. A list of lists or structs needs to be provided as json encoded string

cmd = click.option(f'--{param_name}', multiple=True)(cmd)
elif param.annotation == dict:
cmd = json_option(f'--{param_name}')(cmd)
else:
# print param
#print(f"Unsupported type: {param.annotation}")
cmd = click.option(f'--{param_name}')(cmd)
return cmd

# Define the call commands
call_commands = {
"get": {"method": Call.get},
"update": {"method": Call.update},
"delete": {"method": Call.delete},
"get_or_create": {"method": Call.get_or_create},
# Add more call commands as needed
}

# Define the video commands
video_commands = {
"query_call_members": {"method": VideoClient.query_call_members},
"query_call_stats": {"method": VideoClient.query_call_stats},
"query_calls": {"method": VideoClient.query_calls},
"list_call_types": {"method": VideoClient.list_call_types},
"create_call_type": {"method": VideoClient.create_call_type},
"delete_call_type": {"method": VideoClient.delete_call_type},
"get_call_type": {"method": VideoClient.get_call_type},
"update_call_type": {"method": VideoClient.update_call_type},
"get_edges": {"method": VideoClient.get_edges},
# Add more video commands as needed
}

# Create the commands
call_cmds = [create_call_command(name, command["method"]) for name, command in call_commands.items()]
video_cmds = [create_video_command(name, command["method"]) for name, command in video_commands.items()]


# Create a group for call commands
@click.group()
def call():
"""Commands for specific calls"""
pass

for cmd in call_cmds:
call.add_command(cmd)

# Add the commands to the CLI group
@click.group()
def video():
"""Video-related commands"""
pass

video.add_command(call)

for cmd in video_cmds:
video.add_command(cmd)

@click.command()
@click.option("--rtmp-user-id", default=f"{uuid.uuid4()}")
@pass_client
def rtmp_in_setup(client: Stream, rtmp_user_id: str):
call = client.video.call("default", f"rtmp-in-{uuid.uuid4()}").get_or_create(
data=CallRequest(
created_by_id=rtmp_user_id,
),
)
print(f"RTMP URL: {call.data.call.ingress.rtmp.address}")
print(
f"RTMP Stream Token: {client.create_call_token(user_id=rtmp_user_id, call_cids=[call.data.call.cid])}"
)
print(f"React call link: https://pronto.getstream.io/join/{call.data.call.id}")


video.add_command(rtmp_in_setup)
Loading
Loading