diff --git a/evalai/__init__.py b/evalai/__init__.py index e69de29bb..2652e4445 100644 --- a/evalai/__init__.py +++ b/evalai/__init__.py @@ -0,0 +1 @@ +name = "evalai" diff --git a/evalai/auth.py b/evalai/auth.py deleted file mode 100644 index 5020b43ba..000000000 --- a/evalai/auth.py +++ /dev/null @@ -1,9 +0,0 @@ -import click - -from click import echo - - -@click.command() -def auth(): - """Example script.""" - echo('Hello Auth!') diff --git a/evalai/challenges.py b/evalai/challenges.py index e53e4e964..efec22d96 100644 --- a/evalai/challenges.py +++ b/evalai/challenges.py @@ -1,9 +1,66 @@ import click - from click import echo +from evalai.utils.challenges import ( + get_challenge_list, + get_ongoing_challenge_list, + get_past_challenge_list, + get_future_challenge_list,) + + +@click.group(invoke_without_command=True) +@click.pass_context +def challenges(ctx): + """ + Challenges and related options. + """ + if ctx.invoked_subcommand is None: + welcome_text = """Welcome to the EvalAI CLI. Use evalai challenges --help for viewing all the options.""" + echo(welcome_text) + + +@click.group(invoke_without_command=True, name='list') +@click.pass_context +def list_challenges(ctx): + """ + Used to list all the challenges. + Invoked by running `evalai challenges list` + """ + if ctx.invoked_subcommand is None: + get_challenge_list() + + +@click.command(name='ongoing') +def list_ongoing_challenges(): + """ + Used to list all the challenges which are active. + Invoked by running `evalai challenges list ongoing` + """ + get_ongoing_challenge_list() + + +@click.command(name='past') +def list_past_challenges(): + """ + Used to list all the past challenges. + Invoked by running `evalai challenges list past` + """ + get_past_challenge_list() + + +@click.command(name='future') +def list_future_challenges(): + """ + Used to list all the challenges which are coming up. + Invoked by running `evalai challenges list future` + """ + get_future_challenge_list() + + +# Command -> evalai challenges list +challenges.add_command(list_challenges) -@click.command() -def challenges(): - """Example script.""" - echo('Hello Challenges!') +# Command -> evalai challenges list ongoing/past/future +list_challenges.add_command(list_ongoing_challenges) +list_challenges.add_command(list_past_challenges) +list_challenges.add_command(list_future_challenges) diff --git a/evalai/main.py b/evalai/main.py index 1ead0d601..f3f56e18a 100644 --- a/evalai/main.py +++ b/evalai/main.py @@ -2,7 +2,6 @@ from click import echo -from .auth import auth from .challenges import challenges from .submissions import submissions from .teams import teams @@ -11,12 +10,15 @@ @click.group(invoke_without_command=True) @click.pass_context def main(ctx): + """ + Welcome to the EvalAI CLI. + """ if ctx.invoked_subcommand is None: - echo('I was invoked without subcommand') - else: - echo('I am about to invoke %s' % ctx.invoked_subcommand) + welcome_text = """Welcome to the EvalAI CLI. Use evalai --help for viewing all the options""" + echo(welcome_text) -main.add_command(auth) + +# Command -> evalai auth/challenges/submissions/teams main.add_command(challenges) main.add_command(submissions) main.add_command(teams) diff --git a/evalai/utils/__init__.py b/evalai/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/evalai/utils/auth.py b/evalai/utils/auth.py index e69de29bb..b3f2bd6b8 100644 --- a/evalai/utils/auth.py +++ b/evalai/utils/auth.py @@ -0,0 +1,39 @@ +import os +import json +from os.path import expanduser + +from click import echo + + +AUTH_TOKEN = 'token.json' +AUTH_TOKEN_PATH = "{}/.evalai/{}".format(expanduser('~'), AUTH_TOKEN) + + +def get_token(): + """ + Loads token to be used for sending requests. + """ + if os.path.exists(AUTH_TOKEN_PATH): + with open(str(AUTH_TOKEN_PATH), 'r') as TokenObj: + try: + data = TokenObj.read() + except (OSError, IOError) as e: + echo(e) + data = json.loads(data) + token = data["token"] + return token + else: + echo("\nYour token file doesn't exists.") + echo("\nIt should be present at ~/.evalai/token.json\n") + return None + + +def get_headers(): + """ + Returns token formatted in header for sending requests. + """ + headers = { + "Authorization": "Token {}".format(get_token()), + } + + return headers diff --git a/evalai/utils/challenges.py b/evalai/utils/challenges.py index e69de29bb..3a8810b74 100644 --- a/evalai/utils/challenges.py +++ b/evalai/utils/challenges.py @@ -0,0 +1,86 @@ +import os +import requests +import sys + +from click import echo, style + +from pylsy import pylsytable + +from evalai.utils.auth import get_headers +from evalai.utils.urls import Urls +from evalai.utils.common import valid_token + + +API_HOST_URL = os.environ.get("EVALAI_API_URL", 'http://localhost:8000') + + +def print_challenge_table(challenge): + br = style("------------------------------------------------------------------", bold=True) + + challenge_title = "\n{}".format(style(challenge["title"], bold=True, fg="green")) + challenge_id = "ID: {}\n\n".format(style(str(challenge["id"]), bold=True, fg="blue")) + + title = "{} {}".format(challenge_title, challenge_id) + + description = "{}\n".format(challenge["short_description"]) + date = "End Date : " + style(challenge["end_date"].split("T")[0], fg="red") + date = "\n{}\n\n".format(style(date, bold=True)) + challenge = "{}{}{}{}".format(title, description, date, br) + return challenge + + +def get_challenges(url): + + headers = get_headers() + try: + response = requests.get(url, headers=headers) + response.raise_for_status() + except requests.exceptions.HTTPError as err: + echo(err) + sys.exit(1) + except requests.exceptions.RequestException as err: + echo(err) + sys.exit(1) + + response_json = response.json() + if valid_token(response_json): + + challenges = response_json["results"] + if len(challenges) is not 0: + for challenge in challenges: + challenge = print_challenge_table(challenge) + echo(challenge) + else: + echo("Sorry, no challenges found.") + + +def get_challenge_list(): + """ + Fetches the list of challenges from the backend. + """ + url = "{}{}".format(API_HOST_URL, Urls.challenge_list.value) + get_challenges(url) + + +def get_past_challenge_list(): + """ + Fetches the list of past challenges from the backend. + """ + url = "{}{}".format(API_HOST_URL, Urls.past_challenge_list.value) + get_challenges(url) + + +def get_ongoing_challenge_list(): + """ + Fetches the list of ongoing challenges from the backend. + """ + url = "{}{}".format(API_HOST_URL, Urls.challenge_list.value) + get_challenges(url) + + +def get_future_challenge_list(): + """ + Fetches the list of future challenges from the backend. + """ + url = "{}{}".format(API_HOST_URL, Urls.future_challenge_list.value) + get_challenges(url) diff --git a/evalai/utils/common.py b/evalai/utils/common.py new file mode 100644 index 000000000..3c60cf87e --- /dev/null +++ b/evalai/utils/common.py @@ -0,0 +1,16 @@ +from click import echo + + +def valid_token(response): + """ + Checks if token is valid. + """ + + if ('detail' in response): + if (response['detail'] == 'Invalid token'): + echo("The authentication token you are using isn't valid. Please try again.") + return False + if (response['detail'] == 'Token has expired'): + echo("Sorry, the token has expired.") + return False + return True diff --git a/evalai/utils/urls.py b/evalai/utils/urls.py index e69de29bb..23ac8f412 100644 --- a/evalai/utils/urls.py +++ b/evalai/utils/urls.py @@ -0,0 +1,6 @@ +from enum import Enum + +class Urls(Enum): + challenge_list = "/api/challenges/challenge/all" + past_challenge_list = "/api/challenges/challenge/past" + future_challenge_list = "/api/challenges/challenge/future" diff --git a/setup.py b/setup.py index abb4c8479..f96781a62 100644 --- a/setup.py +++ b/setup.py @@ -10,25 +10,21 @@ setup( name=PROJECT, - version='1.0', + version='1.6a1', description='Use EvalAI through the CLI!', + long_description=long_description, author='Cloud-CV', author_email='team@cloudcv.org', - url='https://github.com/Cloud-CV/evalai_cli', - download_url='https://github.com/Cloud-CV/evalai_cli/tarball/master', + url='https://github.com/Cloud-CV/evalai_cli ', - classifiers=['Development Status :: 1 - Alpha', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Intended Audience :: Developers', - 'Environment :: Console', - ], + classifiers=( + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ), platforms=['Any'], @@ -36,11 +32,12 @@ provides=[], install_requires=[ - 'click==6.7', - 'pandas==0.22.0', - 'pylsy==3.6', - 'requests==2.18.4', - 'responses==0.9.0', + 'click', + 'colorama', + 'pandas', + 'pylsy', + 'requests', + 'responses', ], namespace_packages=[], diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/challenge_response.py b/tests/data/challenge_response.py new file mode 100644 index 000000000..fa19eb248 --- /dev/null +++ b/tests/data/challenge_response.py @@ -0,0 +1,17 @@ +challenges = """ +{'results': [{'is_active': True, 'anonymous_leaderboard': False, 'blocked_email_domains': [], 'submission_guidelines': 'Perspiciatis id sunt ab magni rerum laboriosam. Alias temporibus ratione est animi. Quisqua\ +m reiciendis impedit fugiat corporis nesciunt totam. Odit molestiae praesentium et fuga architecto suscipit. At deleniti fugiat necessitatibus vel provident qui perspiciatis.', 'title': 'Olivia Challenge',\ +'enable_forum': True, 'short_description': 'Ratione laboriosam quae tempora. Temporibus porro repellat rem facere. In impedit cupiditate voluptatum aut omnis animi illo. Perferendis ratione dolores eaque nulla iusto\ +mollitia facere voluptatum. Earum dolor corporis quo enim quia optio.', 'image': None, 'evaluation_details': 'Amet officia saepe quis tempora magnam eum. Quidem ab consectetur exercitationem omnis. Nostrum\ +consequuntur architecto eaque mollitia ab minima expedita quam. Velit itaque voluptates suscipit aliquam perspiciatis itaque cupiditate.', 'description': 'Excepturi eligendi minus modi delectus dolore\ +asperiores voluptatem. Aspernatur itaque vitae repellendus. Natus ut tenetur labore dolores ex repudiandae.', 'id': 2, 'start_date': '2018-02-02T18:56:42.747134Z', 'terms_and_conditions': 'Est vero fugiat\ +temporibus necessitatibus. Ea nihil possimus consequuntur doloribus odio. Vero voluptates non et repellat perferendis ipsam. Ex dicta nemo numquam cupiditate recusandae impedit.', 'allowed_email_domains': [], 'end_date': '2019-09\ +25T18:56:42.747145Z', 'creator': {'team_name': 'South Lisafurt Host Team', 'created_by': 'host', 'id': 2}, 'approved_by_admin': True, 'published': True}, {'is_active': False, 'anonymous_leaderboard': False,\ +'blocked_email_domains': [], 'submission_guidelines': 'Ullam vitae explicabo consequuntur odit fugiat pariatur doloribus ab. Qui ullam adipisci est corporis facilis. Quas excepturi deleniti\ +dolorum tempora necessitatibus.', 'title': 'Jason Challenge', 'enable_forum': True, 'short_description': 'Dicta tempore quia itaque ex quam. Quas sequi in voluptates esse aspernatur deleniti. In magnam ipsam totam ratione quidem\ +praesentium eius distinctio.', 'image': None, 'evaluation_details': 'Adipisci possimus tenetur illum maiores. Laboriosam error nostrum illum nesciunt cumque officiis suscipit. Occaecati velit fugiat alias magnam\ +voluptas voluptatem ad. Repudiandae velit impedit veniam numquam.', 'description': 'Voluptates consequatur commodi odit repellendus quam. Id nemo provident ipsa cupiditate enim blanditiis autem. Recusandae vero\ +necessitatibus debitis esse eveniet consequatur. Provident saepe officiis incidunt cum.', 'id': 3, 'start_date': '2016-12-29T18:56:42.752783Z', 'terms_and_conditions': 'Recusandae saepe ipsum saepe ullam aut. Cum eius\ +nihil blanditiis itaque. Fugiat sed quod nostrum.', 'allowed_email_domains': [], 'end_date': '2018-02-02T18:56:42.752795Z', 'creator': {'team_name': 'South Lisafurt Host Team', 'created_by': 'host', 'id': 2},\ +'approved_by_admin': True, 'published': True}], 'next': None, 'count': 10, 'previous': None} +""" diff --git a/tests/test_challenges.py b/tests/test_challenges.py index e69de29bb..36269d480 100644 --- a/tests/test_challenges.py +++ b/tests/test_challenges.py @@ -0,0 +1,84 @@ +import ast +import click +import responses +import subprocess + +from click.testing import CliRunner +from pylsy import pylsytable + +from evalai.challenges import challenges +from tests.data import challenge_response + +from evalai.utils.challenges import API_HOST_URL +from evalai.utils.urls import Urls + + +class TestChallenges: + + def setup(self): + + json_data = ast.literal_eval(challenge_response.challenges) + + url = "{}{}" + responses.add(responses.GET, url.format(API_HOST_URL, Urls.challenge_list.value), + json=json_data, status=200) + + responses.add(responses.GET, url.format(API_HOST_URL, Urls.past_challenge_list.value), + json=json_data, status=200) + + responses.add(responses.GET, url.format(API_HOST_URL, Urls.challenge_list.value), + json=json_data, status=200) + + responses.add(responses.GET, url.format(API_HOST_URL, Urls.future_challenge_list.value), + json=json_data, status=200) + + challenges = json_data["results"] + + self.output = "" + + title = "\n{}".format("{}") + idfield = "{}\n\n".format("{}") + subtitle = "\n{}\n\n".format("{}") + br = "------------------------------------------------------------------\n" + + for challenge in challenges: + challenge_title = title.format(challenge["title"]) + challenge_id = "ID: " + idfield.format(challenge["id"]) + + heading = "{} {}".format(challenge_title, challenge_id) + description = "{}\n".format(challenge["short_description"]) + date = "End Date : " + challenge["end_date"].split("T")[0] + date = subtitle.format(date) + challenge = "{}{}{}{}".format(heading, description, date, br) + + self.output = self.output + challenge + + + @responses.activate + def test_challenge_lists(self): + runner = CliRunner() + result = runner.invoke(challenges, ['list']) + response_table = result.output + assert response_table == self.output + + + @responses.activate + def test_challenge_lists_past(self): + runner = CliRunner() + result = runner.invoke(challenges, ['list', 'past']) + response_table = result.output + assert response_table == self.output + + @responses.activate + def test_challenge_lists_ongoing(self): + runner = CliRunner() + result = runner.invoke(challenges, ['list', 'ongoing']) + response_table = result.output + assert response_table == self.output + + @responses.activate + def test_challenge_lists_future(self): + runner = CliRunner() + result = runner.invoke(challenges, ['list', 'future']) + response_table = result.output + assert response_table == self.output