diff --git a/CHANGELOG.md b/CHANGELOG.md index fce9d8da..00f713be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +0.1.9 +------------------- +* Improved support by static analyzers +* Added default value for `Pool.defaults` attribute +* Added default value 0 for `real_tasks_count`, `golden_tasks_count`, `training_tasks_count` in `MixerConfig` +* Added default value `AUTOMATED` for `Project.assignments_issuing_type` attribute +* Simplified `TolokaPluginV1`' interface by expanding `layout` attribute +* Simplified `TemplateBuilderViewSpec`'s interface by expanding `config` attribute +* Fixed an issue with `TemplateBuilder` config displayed in one line in Toloka's web interface +* `City`, `Languages`, `RegionByPhone` and `RegionByIp` filters now have `include` and `exclude`. Thix methods will eventually replace misleading `in_` and `not_in` method names. As for now, all variants are available for backward compatibility +* Retry Toloka quotas. Minute quotas are retried by default. And you can turn on the retrying of hourly and daily quotas. + + 0.1.8 ------------------- * Added `get_aggregated_solutions` method diff --git a/README.md b/README.md index ca81f6ee..ecf7f8ce 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ Designed by engineers for engineers, Toloka lets you integrate an on-demand work Main advantages of Toloka: - **Top-quality data** - Collect and annotate training data that meets and exceeds industry quality standards thanks to multiple quality control methods and mechanisms available in Toloka. - - **Scalable projects** - Have any amounts of image, text, speech, audio or video data collected and labeled for you by millions of skilled Toloka users across the globe. - - **Cost-efficiency** - Save time and money with this purpose-built platform for handling large-scale data collection and annotation projects, on demand 24/7, at your own price and within your timeframe. + - **Scalable projects** - Have any amounts of image, text, speech, audio, or video data collected and labeled for you by millions of skilled Toloka users across the globe. + - **Cost-efficiency** - Save time and money with this purpose-built platform for handling large-scale data collection and annotation projects, on-demand 24/7, at your own price and within your timeframe. - **Free, powerful API** - Build scalable and fully automated human-in-the-loop machine learning pipelines with a powerful open API. @@ -40,7 +40,7 @@ Installing toloka-kit is as easy as: ``` $ pip install toloka-kit ``` -Note: this project is still under heavy development and interfaces may change slightly. For production environments please specify exact package version such as `toloka-kit==0.1.3` +Note: this project is still under heavy development and interfaces may change slightly. For production environments please specify exact package version. For example: `toloka-kit==0.1.8` **Try your first program and checks the validity of the OAuth token:** ```python @@ -56,7 +56,7 @@ Useful Links - [Toloka requester's guide.](https://yandex.ru/support/toloka-requester/index.html?lang=en) - We recommend that you first get acquainted with Toloka through the web interface and implement [one of the tutorials.](https://yandex.ru/support/toloka-requester/concepts/usecases.html) - [Toloka API documentation.](https://yandex.com/dev/toloka/doc/concepts/about.html?lang=en) -- [Toloka-kit usage examples.](https://github.com/Toloka/toloka-kit/tree/main/examples) +- [Toloka-kit usage examples.](https://github.com/Toloka/toloka-kit/tree/main/examples#toloka-kit-usage-examples) Questions and bug reports -------------- @@ -65,6 +65,9 @@ Questions and bug reports * Seek prompt advice at English-speaking [Telegram chat](https://t.me/toloka_tech) (Mostly tech question) +Contributing +------- +Feel free to contribute to toloka-kit. Right now, we really need more [usage examples.](https://github.com/Toloka/toloka-kit/tree/main/examples#need-more-examples) License ------- diff --git a/examples/image_gathering/image_gathering.ipynb b/examples/1.computer_vision/image_collection/image_collection.ipynb similarity index 77% rename from examples/image_gathering/image_gathering.ipynb rename to examples/1.computer_vision/image_collection/image_collection.ipynb index 8e9f33f9..8eb612e3 100644 --- a/examples/image_gathering/image_gathering.ipynb +++ b/examples/1.computer_vision/image_collection/image_collection.ipynb @@ -1,6 +1,8 @@ { "cells": [ { + "cell_type": "markdown", + "metadata": {}, "source": [ "# How to collect images for a dataset\n", "\n", @@ -9,34 +11,44 @@ "Performers will be asked to take a photo of their pet and specify the type of animal.\n", "\n", "The real project like that should be subdivided into subprojects of validation and markup to make sure each photo is correct and contains the object it says it does. This example is simplified and doesn't contain subdivision." - ], + ] + }, + { "cell_type": "markdown", - "metadata": {} + "metadata": {}, + "source": [ + "Prepare the environment and import everything you'll need." + ] }, { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ - "# Prepare the environment and import everything you'll need\n", - "!pip install toloka-kit==0.1.5\n", + "!pip install toloka-kit==0.1.7\n", "\n", "import datetime\n", + "import logging\n", + "import sys\n", "import time\n", "\n", "import toloka.client as toloka\n", - "import toloka.client.project.template_builder as tb" - ], - "cell_type": "code", - "metadata": {}, - "execution_count": null, - "outputs": [] + "import toloka.client.project.template_builder as tb\n", + "\n", + "logging.basicConfig(\n", + " format='[%(levelname)s] %(name)s: %(message)s',\n", + " level=logging.INFO,\n", + " stream=sys.stdout,\n", + ")" + ] }, { - "source": [ - "Click [here](https://github.com/Toloka/toloka-kit/blob/main/README.md) to learn about Toloka and how to get an OAuth token.\n", - "\n", - "[Image segmentation example](https://github.com/Toloka/toloka-kit/blob/main/examples/image_segmentation/image_segmentation.ipynb)." - ], "cell_type": "markdown", - "metadata": {} + "metadata": {}, + "source": [ + "Сreate toloka-client instance. All api calls will go through it. More about OAuth token in our [Learn the basics example](https://github.com/Toloka/toloka-kit/tree/main/examples/0.getting_started/0.learn_the_basics) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Toloka/toloka-kit/blob/main/examples/0.getting_started/0.learn_the_basics/learn_the_basics.ipynb)" + ] }, { "cell_type": "code", @@ -44,30 +56,24 @@ "metadata": {}, "outputs": [], "source": [ - "# Сreate a toloka-client instance\n", - "# All API calls will go through it\n", - "try:\n", - " token = input(\"Enter your token:\")\n", - " toloka_client = toloka.TolokaClient(token, 'PRODUCTION') # Or switch to 'SANDBOX'\n", - " # Lines below check the availability of money in your account and that the OAuth token is correct\n", - " requester = toloka_client.get_requester()\n", - " print('It\\'s enough money on your account - ', requester.balance > 3.0)\n", - "except:\n", - " print('You probably entered an invalid token. Please, run this cell again.')" + "toloka_client = toloka.TolokaClient(input(\"Enter your token:\"), 'PRODUCTION') # Or switch to 'SANDBOX'\n", + "print(toloka_client.get_requester())" ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "---\n", "---\n", "## Starting a project\n", "\n", "Note: Go to the next section to get results for the **already launched project**." - ], - "cell_type": "markdown", - "metadata": {} + ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "### Create a new project\n", "\n", @@ -76,12 +82,8 @@ "The task interface should:\n", "- Contain the description of the task.\n", "- Permit uploading images.\n", - "- Allow to select the type of object depicted in the image.\n", - "\n", - "Structure of output data:" - ], - "cell_type": "markdown", - "metadata": {} + "- Allow to select the type of object depicted in the image." + ] }, { "cell_type": "code", @@ -89,20 +91,21 @@ "metadata": {}, "outputs": [], "source": [ - "output_specification = {\n", - " 'image': toloka.project.field_spec.FileSpec(),\n", - " 'label': toloka.project.field_spec.StringSpec(allowed_values=['cat', 'dog', 'none'])\n", - "}" + "new_project = toloka.project.Project(\n", + " assignments_issuing_type=toloka.project.Project.AssignmentsIssuingType.AUTOMATED,\n", + " public_name='Take a photo of your pet',\n", + " public_description='If you have a cat or a dog, take a picture of it. If you don\\'t have any such animals, take a random photo.',\n", + ")" ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "Configure the task interface.\n", "\n", "Click [here](https://yandex.com/support/toloka-tb/index.html) to learn more about Template Builder, an environment for task interface configuration." - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -113,18 +116,18 @@ "# Radio buttons to choose the label type\n", "radio_group_field = tb.fields.RadioGroupFieldV1(\n", " data=tb.data.OutputData(path='label'),\n", - " label='What\\'s in your photograph?',\n", + " label='What is in your photograph?',\n", " validation=tb.conditions.RequiredConditionV1(),\n", " options=[\n", " tb.fields.GroupFieldOption(label='Cat', value='cat'),\n", " tb.fields.GroupFieldOption(label='Dog', value='dog'),\n", - " tb.fields.GroupFieldOption(label='Neither a cat nor a dog', value='none'),\n", + " tb.fields.GroupFieldOption(label='Not a cat nor a dog', value='none'),\n", " ]\n", ")\n", "\n", "# Buttons for loading an image or taking a photo\n", "image_loader = tb.fields.MediaFileFieldV1(\n", - " label='Upload a photo of your cat or your dog. Read the instructions carefully.',\n", + " label='Upload a photo of your cat or dog. Read the instructions carefully.',\n", " data=tb.data.OutputData(path='image'),\n", " validation=tb.conditions.RequiredConditionV1(),\n", " accept=tb.fields.MediaFileFieldV1.Accept(photo=True, gallery=True),\n", @@ -149,42 +152,80 @@ " 'showFullscreen': True,\n", " 'showInstructions': True,\n", " },\n", - ")\n", - "\n", - "public_instruction = \"\"\"Take a picture of your pet if it is a cat or a dog and select the appropriate label type.

\n", - "If you don't have a cat or a dog, take a photo of anything and select a \"Not a cat nor a dog\" label. There should be exactly one animal in the photo, clearly visible, not cropped. The animal can be photographed from any side and in any position. You can take a picture of a pet in your arms.

\n", - "It should be clearly visible what animal is depicted (e.g. do not photograph your pet's back in the dark).\n", - "\"\"\"\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Set data specification. And set task interface to project." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "input_specification = {'label': toloka.project.field_spec.StringSpec(required=False, hidden=True)}\n", + "output_specification = {\n", + " 'image': toloka.project.field_spec.FileSpec(),\n", + " 'label': toloka.project.field_spec.StringSpec(allowed_values=['cat', 'dog', 'none'])\n", + "}\n", "\n", - "# Create a project\n", - "new_project = toloka.project.Project(\n", - " assignments_issuing_type=toloka.project.Project.AssignmentsIssuingType.AUTOMATED,\n", - " public_name='Take a photo of your pet',\n", - " public_description='If you have a cat or a dog, take a picture of it. If you don\\'t have any such animals, take a random photo.',\n", - " public_instructions=public_instruction,\n", - " # Set up the task interface and output parameters\n", - " task_spec=toloka.project.task_spec.TaskSpec(\n", - " input_spec={'label': toloka.project.field_spec.StringSpec(required=False, hidden=True)},\n", + "new_project.task_spec = toloka.project.task_spec.TaskSpec(\n", + " input_spec=input_specification,\n", " output_spec=output_specification,\n", " view_spec=project_interface,\n", - " ),\n", - ")\n", - "\n", - "# An API request to create a new project\n", - "new_project = toloka_client.create_project(new_project)\n", - "print(f'Created project with id {new_project.id}')\n", - "print(f'To view the project, go to https://toloka.yandex.com/requester/project/{new_project.id}')\n", - "# print(f'To view this pool, go to https://sandbox.toloka.yandex.com/requester/project/{new_project.id}/pool/{new_pool.id}') # Print a sandbox version link" + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Write short and simple instructions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "new_project.public_instructions = \"\"\"Take a picture of your pet if it is a cat or a dog and select the appropriate label type.

\n", + "If you don't have a cat or a dog, take a photo of anything and select a \"Not a cat nor a dog\" label. There should be exactly one animal in the photo, clearly visible, not cropped. The animal can be photographed from any side and in any position. You can take a picture of a pet in your arms.

\n", + "It should be clearly visible what animal is depicted (e.g. do not photograph your pet's back in the dark).\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a project via API request." ] }, { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ - "### Create a pool" - ], + "new_project = toloka_client.create_project(new_project)" + ] + }, + { "cell_type": "markdown", - "metadata": {} + "metadata": {}, + "source": [ + "### Pool creation" + ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "Create a task pool and set its quality control rules.\n", "\n", @@ -192,9 +233,7 @@ "\n", "1. A performer gets the skill after sending a response.\n", "2. The performers with the skill are not allowed to perform the task." - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -217,6 +256,8 @@ ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "Access to tasks is granted for:\n", "\n", @@ -227,9 +268,7 @@ "2. English-speaking performers.\n", "\n", " _Why: The task instruction is written in English._" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -252,8 +291,22 @@ " (toloka.filter.Skill(pet_skill.id) == None) &\n", " (toloka.filter.ClientType == toloka.filter.ClientType.ClientType.TOLOKA_APP)\n", " ),\n", - ")\n", - "\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Set up the Submitted responses quality control rule. When a persons submit 1 or more tasks, they are assigned the skill created above." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "# Automatically updating skills\n", "new_pool.quality_control.add_action(\n", " collector=toloka.collectors.AnswerCount(),\n", @@ -261,15 +314,28 @@ " conditions=[toloka.conditions.AssignmentsAcceptedCount > 0],\n", " # It doesn't add to the skill, it sets the new skill to 1\n", " action=toloka.actions.SetSkill(skill_id=pet_skill.id, skill_value=1),\n", - ")\n", - "\n", - "new_pool = toloka_client.create_pool(new_pool)\n", - "print(f'Created pool with id {new_pool.id}')\n", - "print(f'To view the pool, go to https://toloka.yandex.com/requester/project/{new_project.id}/pool/{new_pool.id}')\n", - "# print(f'To view this pool, go to https://sandbox.toloka.yandex.com/requester/project/{new_project.id}/pool/{new_pool.id}') # Print a sandbox version link" + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a pool." ] }, { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "new_pool = toloka_client.create_pool(new_pool)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, "source": [ "Open the project for preview.\n", "\n", @@ -286,19 +352,17 @@ "\n", "\n", "Note: In preview mode you won't be able to upload an image and look at the result. This restriction is related to the preview features and doesn't affect performers." - ], - "cell_type": "markdown", - "metadata": {} + ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "### Add a task and run the project\n", "Add one task.\n", "\n", "Adjust the amount of images you want to get by changing the overlap." - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -306,7 +370,7 @@ "metadata": {}, "outputs": [], "source": [ - "image_count = 10 # How many images you will receive.\n", + "image_count = 5 # How many images you will receive.\n", "new_tasks_suite = toloka.task_suite.TaskSuite(\n", " pool_id=new_pool.id,\n", " tasks=[toloka.task.Task(input_values={'label': 'Cats vs Dogs'})],\n", @@ -315,9 +379,6 @@ "\n", "# Add task suites to the pool\n", "toloka_client.create_task_suite(new_tasks_suite)\n", - "print(f'Created pool with id {new_pool.id}')\n", - "print(f'To view this pool, go to https://toloka.yandex.com/requester/project/{new_project.id}/pool/{new_pool.id}')\n", - "# print(f'To view this pool, go to https://sandbox.toloka.yandex.com/requester/project/{new_project.id}/pool/{new_pool.id}') # Print a sandbox version link\n", "\n", "# Open the pool\n", "new_pool = toloka_client.open_pool(new_pool.id)\n", @@ -325,15 +386,17 @@ ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "## Getting responses\n", "\n", "Wait for performers to complete the tasks, then download the results." - ], - "cell_type": "markdown", - "metadata": {} + ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "### If your work with the notepad was interrupted\n", "\n", @@ -343,9 +406,7 @@ "2. Run all the code cells.\n", "\n", "If you are executing the notepad right now, **skip the next cell**." - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -357,15 +418,19 @@ ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "### Wait for the responses\n", "\n", "Wait for all the tasks in the pool to be completed." - ], - "cell_type": "markdown", - "metadata": {} + ] }, { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "def wait_pool_for_close(pool_id, minutes_to_wait=1):\n", " sleep_time = 60 * minutes_to_wait\n", @@ -383,20 +448,16 @@ " print('Pool was closed.')\n", "\n", "wait_pool_for_close(pool_id)" - ], - "cell_type": "code", - "metadata": {}, - "execution_count": null, - "outputs": [] + ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ - "Download the results. \n", + "Download the results.\n", "\n", "Note: You should download files' ids, not the files themselves. The files will only be needed right before reviewing." - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -415,35 +476,35 @@ ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "---\n", "---\n", "## Showing results\n", "\n", "Configure data display." - ], - "cell_type": "markdown", - "metadata": {} + ] }, { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "!pip install ipyplot\n", "from PIL import Image, ImageDraw\n", "import ipyplot\n", "\n", "results_iter = iter(results_list)" - ], - "cell_type": "code", - "metadata": {}, - "execution_count": null, - "outputs": [] + ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "Run the cell below multiple times to see different responses." - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -467,6 +528,8 @@ ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "## Summary\n", "\n", @@ -475,9 +538,7 @@ "In real projects you should configure:\n", "- Non-automatic acceptance to have the time to review the images.\n", "- Linked project for validation and object's type markup." - ], - "cell_type": "markdown", - "metadata": {} + ] } ], "metadata": { @@ -496,9 +557,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.0" + "version": "3.9.1" } }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/examples/1.computer_vision/image_collection/img/performer_interface.png b/examples/1.computer_vision/image_collection/img/performer_interface.png new file mode 100644 index 00000000..1d3a507f Binary files /dev/null and b/examples/1.computer_vision/image_collection/img/performer_interface.png differ diff --git a/examples/1.computer_vision/side_by_side_comparison/img/performer_interface.png b/examples/1.computer_vision/side_by_side_comparison/img/performer_interface.png new file mode 100644 index 00000000..addfc496 Binary files /dev/null and b/examples/1.computer_vision/side_by_side_comparison/img/performer_interface.png differ diff --git a/examples/1.computer_vision/side_by_side_comparison/img/possible_results.png b/examples/1.computer_vision/side_by_side_comparison/img/possible_results.png new file mode 100644 index 00000000..7ac84721 Binary files /dev/null and b/examples/1.computer_vision/side_by_side_comparison/img/possible_results.png differ diff --git a/examples/1.computer_vision/side_by_side_comparison/side_by_side_comparison.ipynb b/examples/1.computer_vision/side_by_side_comparison/side_by_side_comparison.ipynb new file mode 100644 index 00000000..a8625eb5 --- /dev/null +++ b/examples/1.computer_vision/side_by_side_comparison/side_by_side_comparison.ipynb @@ -0,0 +1,597 @@ +{ + "cells": [ + { + "source": [ + "# Side-by-side image comparison\n", + "We have a set of icons.\n", + "We need to find out which icon people prefer and determine the top icon out of the set.\n", + "We ask performers to look at the icons and choose the one they prefer and then we aggregate these results to obtain the top icon.\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "source": [ + "Prepare environment and import all we'll need." + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "source": [ + "!pip install toloka-kit==0.1.8\n", + "!pip install crowd-kit==0.0.4\n", + "!pip install pandas\n", + "!pip install ipyplot\n", + "\n", + "import datetime\n", + "import itertools\n", + "import sys\n", + "import time\n", + "import logging\n", + "\n", + "import ipyplot\n", + "import pandas\n", + "\n", + "import toloka.client as toloka\n", + "import toloka.client.project.template_builder as tb\n", + "from crowdkit.aggregation import NoisyBradleyTerry \n", + "\n", + "logging.basicConfig(\n", + " format='[%(levelname)s] %(name)s: %(message)s',\n", + " level=logging.INFO,\n", + " stream=sys.stdout,\n", + ")" + ], + "cell_type": "code", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, + { + "source": [ + "Сreate toloka-client instance. All api calls will go through it. More about OAuth token in our [Learn the basics example](https://github.com/Toloka/toloka-kit/tree/main/examples/0.getting_started/0.learn_the_basics) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Toloka/toloka-kit/blob/main/examples/0.getting_started/0.learn_the_basics/learn_the_basics.ipynb)" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "toloka_client = toloka.TolokaClient(input(\"Enter your token:\"), 'PRODUCTION') # Or switch to 'SANDBOX'\n", + "print(toloka_client.get_requester())" + ] + }, + { + "source": [ + "## Create a project" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "project = toloka.Project(\n", + " assignments_issuing_type='AUTOMATED',\n", + " public_name='Which icon do you like more?',\n", + " public_description='Look at the icons and decide which one you like more.',\n", + ")" + ] + }, + { + "source": [ + "Create task interface" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "main_interface = tb.SideBySideLayoutV1(\n", + " items=[\n", + " tb.ImageViewV1(url=tb.InputData(path='image_left'), full_height=True),\n", + " tb.ImageViewV1(url=tb.InputData(path='image_right'), full_height=True),\n", + " ],\n", + " controls=tb.ButtonRadioGroupFieldV1(\n", + " data=tb.OutputData(path='result'),\n", + " label='Which icon do you like more?',\n", + " options=[\n", + " tb.GroupFieldOption(label='Left', value='LEFT'),\n", + " tb.GroupFieldOption(label='Right', value='RIGHT'),\n", + " tb.GroupFieldOption(label='Loading error', value='ERROR'),\n", + " ]\n", + " )\n", + ")\n", + "\n", + "hot_keys_plugin = tb.HotkeysPluginV1(\n", + " key_0=tb.SetActionV1(data=tb.OutputData(path='result'), payload='ERROR'),\n", + " key_1=tb.SetActionV1(data=tb.OutputData(path='result'), payload='LEFT'),\n", + " key_2=tb.SetActionV1(data=tb.OutputData(path='result'), payload='RIGHT'),\n", + ")\n", + "\n", + "project_interface = toloka.project.view_spec.TemplateBuilderViewSpec(\n", + " config=tb.TemplateBuilder(\n", + " view=main_interface,\n", + " plugins=[hot_keys_plugin],\n", + " )\n", + ")" + ] + }, + { + "source": [ + "Set data specification. And set task interface to project." + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "input_specification = {\n", + " 'image_left': toloka.project.field_spec.UrlSpec(),\n", + " 'image_right': toloka.project.field_spec.UrlSpec(),\n", + "}\n", + "output_specification = {'result': toloka.project.field_spec.StringSpec()}\n", + "\n", + "project.task_spec = toloka.project.task_spec.TaskSpec(\n", + " input_spec=input_specification,\n", + " output_spec=output_specification,\n", + " view_spec=project_interface,\n", + ")" + ] + }, + { + "source": [ + "Write short and simple \tinstructions." + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "project.public_instructions = \"\"\"

Look at the icons and decide which one you like more.

\n", + "

Select \"Left\" if you like the icon on the left more.

\n", + "

Select \"Right\" if you like the icon on the right more.

\n", + "

Select \"Loadinf error\" if the picture failed to load.

\"\"\"" + ] + }, + { + "source": [ + "Create a project." + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "project = toloka_client.create_project(project)" + ] + }, + { + "source": [ + "The performer will see the interface like this:\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + " \"Photo\n", + "
\n", + " Figure 1. How performer will see your task\n", + "
" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "source": [ + "## Create a pool\n", + "Specify the [pool parameters.](https://yandex.com/support/toloka-requester/concepts/pool_poolparams.html)" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pool = toloka.Pool(\n", + " project_id=project.id,\n", + " # Give the pool any convenient name. You are the only one who will see it.\n", + " private_name='Which icon do you like more',\n", + " may_contain_adult_content=False,\n", + " # Set the price per task page.\n", + " reward_per_assignment=0.01,\n", + " will_expire=datetime.datetime.utcnow() + datetime.timedelta(days=365),\n", + " # Time given to complete a task suite\n", + " assignment_max_duration_seconds=600,\n", + ")" + ] + }, + { + "source": [ + "Select English-speaking performers" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pool.filter = toloka.filter.Languages.in_('EN')" + ] + }, + { + "source": [ + "Set up [Quality control](https://yandex.com/support/toloka-requester/concepts/control.html). Set up the Submitted responses quality control rule. Restrict the number of responses per user to one. This way you will only get one answer from each user and thus ensure a variety of opinions." + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pool.quality_control.add_action(\n", + " collector=toloka.collectors.AnswerCount(),\n", + " conditions=[toloka.conditions.AssignmentsAcceptedCount == 1],\n", + " action=toloka.actions.RestrictionV2(\n", + " scope=toloka.user_restriction.UserRestriction.PROJECT,\n", + " duration=3,\n", + " duration_unit='DAYS',\n", + " private_comment='No need more answers from this performer',\n", + " )\n", + ")" + ] + }, + { + "source": [ + "Overlap. This is the number of users who will complete the same task. Since you are interested in a variety of opinions, select a big overlap for each task. For example, 10." + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pool.defaults = toloka.Pool.Defaults(default_overlap_for_new_task_suites=10)" + ] + }, + { + "source": [ + "Specify\tthe number of tasks per page. 1 task per page. A performer will only see one pair of images on a page." + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pool.set_mixer_config(\n", + " real_tasks_count=1,\n", + " golden_tasks_count=0,\n", + " training_tasks_count=0\n", + ")" + ] + }, + { + "source": [ + "Create pool" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pool = toloka_client.create_pool(pool)" + ] + }, + { + "source": [ + "## Preparing and uploading tasks\n", + "\n", + "This example uses a small data set with images.\n", + "\n", + "The dataset used is collected by Toloka team and distributed under a Creative Commons Attribution 4.0 International license\n", + "[![License: CC BY 4.0](https://img.shields.io/badge/License-CC%20BY%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by/4.0/)." + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "source": [ + "!curl https://tlk.s3.yandex.net/dataset/toloka_logos/toloka_logos.tsv --output dataset.tsv\n", + "dataset = pandas.read_csv('dataset.tsv', sep='\\t')\n", + "with pandas.option_context(\"max_colwidth\", 80):\n", + " print(dataset)" + ], + "cell_type": "code", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, + { + "source": [ + "Our project is a pairwise comparison of two images. But our dataset contains just a flat list. Let's create a dataset that contains all possible pairs for comparison." + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dataset = pandas.DataFrame(itertools.combinations(dataset['url'], 2), columns=['image_left', 'image_right'])\n", + "with pandas.option_context(\"max_colwidth\", 70):\n", + " display(dataset)" + ] + }, + { + "source": [ + "Create pool tasks" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tasks = [\n", + " toloka.Task(\n", + " pool_id=pool.id,\n", + " input_values={\n", + " 'image_left': row['image_left'],\n", + " 'image_right': row['image_right'],\n", + " }\n", + " )\n", + " for i, row in dataset.iterrows()\n", + "]" + ] + }, + { + "source": [ + "Upload tasks" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "created_tasks = toloka_client.create_tasks(tasks, allow_defaults=True)\n", + "print(len(created_tasks.items))" + ] + }, + { + "source": [ + "Start the pool.\n", + "\n", + "**Important.** Remember that real Toloka performers will complete the tasks.\n", + "Double check that everything is correct\n", + "with your project configuration before you start the pool" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pool = toloka_client.open_pool(pool.id)\n", + "print(pool.status)" + ] + }, + { + "source": [ + "## Receiving responses" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "source": [ + "Wait until the pool is completed." + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "source": [ + "pool_id = pool.id\n", + "\n", + "def wait_pool_for_close(pool_id, minutes_to_wait=1):\n", + " sleep_time = 60 * minutes_to_wait\n", + " pool = toloka_client.get_pool(pool_id)\n", + " while not pool.is_closed():\n", + " op = toloka_client.get_analytics([toloka.analytics_request.CompletionPercentagePoolAnalytics(subject_id=pool.id)])\n", + " op = toloka_client.wait_operation(op)\n", + " percentage = op.details['value'][0]['result']['value']\n", + " print(\n", + " f' {datetime.datetime.now().strftime(\"%H:%M:%S\")}\\t'\n", + " f'Pool {pool.id} - {percentage}%'\n", + " )\n", + " time.sleep(sleep_time)\n", + " pool = toloka_client.get_pool(pool.id)\n", + " print('Pool was closed.')\n", + "\n", + "wait_pool_for_close(pool_id)" + ], + "cell_type": "code", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, + { + "source": [ + "Get responses\n", + "\n", + "When all the tasks are completed, look at the responses from performers." + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "answers = []\n", + "\n", + "for assignment in toloka_client.get_assignments(pool_id=pool_id, status='ACCEPTED'):\n", + " for task, solution in zip(assignment.tasks, assignment.solutions):\n", + " answers.append(\n", + " [\n", + " task.input_values['image_left'],\n", + " task.input_values['image_right'],\n", + " solution.output_values['result'],\n", + " assignment.user_id\n", + " ]\n", + " )\n", + "\n", + "print(f'answers count: {len(answers)}')" + ] + }, + { + "source": [ + "Ranking after a pairwise comparison is quite a difficult task. We will use the Bradley-Terry algorithm, which is already implemented in the Crowd-Kit and allows you to get the result in a few lines of code.\n", + "\n", + "> David R. Hunter. 2004.\n", + "> MM algorithms for generalized Bradley-Terry models\n", + "> Ann. Statist., Vol. 32, 1 (2004): 384–406.\n", + "> \n", + "> \n", + "> Bradley, R. A. and Terry, M. E. 1952.\n", + "> Rank analysis of incomplete block designs. I. The method of paired comparisons.\n", + "> Biometrika, Vol. 39 (1952): 324–345." + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Prepare dataframe\n", + "answers_df = pandas.DataFrame(answers, columns=['left', 'right', 'label', 'performer'])\n", + "\n", + "answers_df = answers_df[(answers_df.label == 'LEFT') | (answers_df.label == 'RIGHT')]\n", + "answers_df['label'] = answers_df.apply(lambda row: row[row['label'].lower()], axis=1)\n", + "\n", + "# Run aggregation\n", + "result = NoisyBradleyTerry().fit_predict(answers_df).sort_values(ascending=False) \n", + "print(result)" + ] + }, + { + "source": [ + "Let's look at the ranking results" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "images = result.index.values\n", + "labels = result.values\n", + "ipyplot.plot_images(\n", + " images=images,\n", + " labels=labels,\n", + " max_images=6,\n", + " img_width=200,\n", + ")" + ] + }, + { + "source": [ + "**You** can see the ranked images. Some possible results are shown in figure 2 below.\n", + "\n", + "\n", + " \n", + " \n", + "
\n", + " \"Possible\n", + "
\n", + " Figure 2. Possible results.\n", + "
" + ], + "cell_type": "markdown", + "metadata": {} + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/examples/README.md b/examples/README.md index 365f9d6c..6c001d75 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,10 +1,10 @@ # Toloka-kit usage examples -## _Data collection, markup, aggregation, and other applications_ +## _Data collection, markup, aggregation, and other applications_ Why it may be usefull: -- Easily reuse projects by just copying and pasting code. No need to configure parameters in the interface over and over again. -- Train your ML models and run your data labeling projects in the same environment. -- Take advantage of open-source code that anyone can use and contribute to. +- Easily reuse projects by just copying and pasting code. No need to configure parameters in the interface over and over again. +- Train your ML models and run your data labeling projects in the same environment. +- Take advantage of open-source code that anyone can use and contribute to. ## Table of content @@ -13,13 +13,15 @@ Why it may be usefull: | [Learn the basics](https://github.com/Toloka/toloka-kit/tree/main/examples/0.getting_started/0.learn_the_basics)

[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Toloka/toloka-kit/blob/main/examples/0.getting_started/0.learn_the_basics/learn_the_basics.ipynb) | The very first example explains the basics of working with Toloka and toloka-kit. Everything is explained by the example of the project on the classification of cats and dogs. |```Getting Started```, ```Classification```| | [Image segmentation/detection](https://github.com/Toloka/toloka-kit/tree/main/examples/image_segmentation)

[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Toloka/toloka-kit/blob/main/examples/image_segmentation/image_segmentation.ipynb) | Example of solving the classic problem of annotating images for training segmentation algorithms. In real-world tasks, annotation is usually done with a polygon. We chose to use a rectangular outline to simplify the task so that we can reduce costs and speed things up. |```CV```, ```Segmentation```, ```Detection```, ```Bounding boxes```, ```Street```, ```Traffic sign```, ```Verification Project```| | [Questing answering on SQuAD](https://github.com/Toloka/toloka-kit/tree/main/examples/SQUAD2.0)

[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Toloka/toloka-kit/blob/main/examples/SQUAD2.0/SQUAD2.0_processing.ipynb) | Solving the problem of question answering on SQUAD2.0 dataset. Collects and validates answers for questions by human performers. One of the most popular tasks in natural language processing. | ```NLP```, ```Questing Answering```, ```Texts```, ```Benchmark```, ```Verification Project```| -| [Image gathering](https://github.com/Toloka/toloka-kit/tree/main/examples/image_gathering)

[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Toloka/toloka-kit/blob/main/examples/image_gathering/image_gathering.ipynb) | The goal for this project is to collect a dataset of dogs' and cats' images. Performers will be asked to take a photo of their pet and specify its species. |```CV```, ```Classification```, ```Collecting```, ```Dataset```| +| [Image collection](https://github.com/Toloka/toloka-kit/tree/main/examples/1.computer_vision/image_collection)

[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Toloka/toloka-kit/blob/main/examples/1.computer_vision/image_collection/image_collection.ipynb) | The goal for this project is to collect a dataset of dogs' and cats' images. Performers will be asked to take a photo of their pet and specify its species. |```CV```, ```Classification```, ```Collecting```, ```Dataset```| | [Simplest Spatial Crowdsourcing](https://github.com/Toloka/toloka-kit/tree/main/examples/2.spatial_crowdsourcing/0.simplest_example)

[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Toloka/toloka-kit/blob/main/examples/2.spatial_crowdsourcing/0.simplest_example/spatial_crowdsourcing.ipynb) | In this example, we will collect pictures of the Moscow metro entrances. This example also can be reused for production tasks such as monitoring the state of objects, checking the presence of an organization or other physical object. |```Spatial Crowdsourcing```, ```Outdoor monitoring```, ```Collecting```| | [ASR/TTS based on Wikipedia articles](https://github.com/noath/asr-datasets-pipeline)

[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/noath/asr-datasets-pipeline/blob/main/ASR_pipeline.ipynb) | This example contains full speech data collecting pipeline from extracting raw texts to labeling and validating speech records. | ```ASR```, ```TTS```, ```Texts```, ```Verification project```, ```Audio samples collection```| | Blood cells classification | Available soon | ```CV```, ```Classification```, ```Medicine```, ```Benchmark```| # Need more examples? -If you have an example of data labeling using toloka-kit, do not hesitate to send us a [pool request](https://github.com/Toloka/toloka-kit/pulls). Ideally, a great example should contain the following aspects: +If you have an example of data labeling using toloka-kit, do not hesitate to send it. Add a link to your GitHub repository and a description to this table via a [pool request](https://github.com/Toloka/toloka-kit/pulls). + +Ideally, a great example should contain the following aspects: - Problem statement; - How to set up a project; - Where to get the data for the example; diff --git a/examples/image_gathering/img/performer_interface.png b/examples/image_gathering/img/performer_interface.png deleted file mode 100644 index bf0504b1..00000000 Binary files a/examples/image_gathering/img/performer_interface.png and /dev/null differ diff --git a/src/client/__init__.py b/src/client/__init__.py index 3eb41614..8d2b610b 100644 --- a/src/client/__init__.py +++ b/src/client/__init__.py @@ -1,4 +1,35 @@ __all__ = [ + 'actions', + 'aggregation', + 'analytics_request', + 'assignment', + 'attachment', + 'batch_create_results', + 'clone_results', + 'collectors', + 'conditions', + 'error_codes', + 'exceptions', + 'filter', + 'message_thread', + 'operation_log', + 'operations', + 'owner', + 'quality_control', + 'requester', + 'search_requests', + 'search_results', + 'skill', + 'solution', + 'task', + 'task_distribution_function', + 'task_suite', + 'training', + 'user_bonus', + 'user_restriction', + 'user_skill', + 'webhook_subscription', + 'TolokaClient', 'Assignment', 'Attachment', @@ -32,16 +63,37 @@ from typing import BinaryIO, Generator, List, Optional, Tuple, Union from urllib3.util.retry import Retry -from . import actions # noqa: F401 +from . import actions from . import aggregation +from . import analytics_request +from . import assignment +from . import attachment from . import batch_create_results -from . import collectors # noqa: F401 -from . import conditions # noqa: F401 +from . import clone_results +from . import collectors +from . import conditions +from . import error_codes +from . import exceptions +from . import filter +from . import message_thread +from . import operation_log from . import operations +from . import owner +from . import quality_control +from . import requester from . import search_requests from . import search_results +from . import skill +from . import solution from . import task +from . import task_distribution_function from . import task_suite +from . import training +from . import user_bonus +from . import user_restriction +from . import user_skill +from . import webhook_subscription + from .__version__ import __version__ from ._converter import structure, unstructure from .aggregation import AggregatedSolution @@ -55,6 +107,7 @@ ) from .operation_log import OperationLogItem from .pool import Pool, PoolPatchRequest +from .primitives.retry import TolokaRetry from .project import Project from .training import Training from .requester import Requester @@ -96,6 +149,12 @@ class TolokaClient: * Set the timeout value to None if you're willing to wait forever. url: If you want to set a specific URL for some reason, for example, for testing. You can only set one parameter, "url" or "environment", not both. + retry_quotas: List of quotas that must be retried. By default retries only minutes quotas. None or empty list for not retrying quotas. + You must set this parameter to None, then you specify the 'retries' parameter as Retry instance. + You can specify quotas: + * MIN - Retry minutes quotas. + * HOUR - Retry hourly quotas. This is means that the program just sleeps for an hour! Be careful. + * DAY - Retry daily quotas. We strongly not recommended retrying these quotas. Example: How to create TolokaClient and make you first request to Toloka. @@ -118,7 +177,8 @@ def __init__( environment: Union[Environment, str, None] = None, retries: Union[int, Retry] = 3, timeout: Union[float, Tuple[float, float]] = 10.0, - url: Optional[str] = None + url: Optional[str] = None, + retry_quotas: Union[List[str], str, None] = TolokaRetry.Unit.MIN ): if url is None and environment is None: raise ValueError('You must pass at least one parameter: url or environment.') @@ -130,10 +190,13 @@ def __init__( if not isinstance(environment, TolokaClient.Environment): environment = TolokaClient.Environment[environment.upper()] self.url = environment.value + if isinstance(retries, Retry) and retry_quotas is not None: + raise ValueError('You must set retry_quotas parameter to None when you specify retries parameters not as int.') self.token = token status_list = [status_code for status_code in requests.status_codes._codes if status_code > 405] if not isinstance(retries, Retry): - retries = Retry( + retries = TolokaRetry( + retry_quotas=retry_quotas, total=retries, status_forcelist=status_list, method_whitelist=['HEAD', 'GET', 'PUT', 'DELETE', 'OPTIONS', 'TRACE', 'POST', 'PATCH'], @@ -860,7 +923,7 @@ def reset_quality_control(quality_control, old_to_new_train_ids): # create trainings new_trainings = [] old_to_new_train_ids = {} - for training in self.get_trainings(project_id=project_id): + for training in self.get_trainings(project_id=project_id): # noqa old_id = training.id training.project_id = new_project.id new_training = self.create_training(training) diff --git a/src/client/__init__.pyi b/src/client/__init__.pyi index 42c242d9..aa70bcbb 100644 --- a/src/client/__init__.pyi +++ b/src/client/__init__.pyi @@ -1,3 +1,52 @@ +__all__ = [ + 'actions', + 'aggregation', + 'analytics_request', + 'assignment', + 'attachment', + 'batch_create_results', + 'clone_results', + 'collectors', + 'conditions', + 'error_codes', + 'exceptions', + 'filter', + 'message_thread', + 'operation_log', + 'operations', + 'owner', + 'quality_control', + 'requester', + 'search_requests', + 'search_results', + 'skill', + 'solution', + 'task', + 'task_distribution_function', + 'task_suite', + 'training', + 'user_bonus', + 'user_restriction', + 'user_skill', + 'webhook_subscription', + + 'TolokaClient', + 'Assignment', + 'Attachment', + 'Folder', + 'MessageThread', + 'MessageThreadReply', + 'MessageThreadFolders', + 'MessageThreadCompose', + 'Skill', + 'TaskSuite', + 'Task', + 'Training', + 'UserBonus', + 'Pool', + 'Project', +] + from datetime import ( datetime, timedelta @@ -167,6 +216,12 @@ class TolokaClient: * Set the timeout value to None if you're willing to wait forever. url: If you want to set a specific URL for some reason, for example, for testing. You can only set one parameter, "url" or "environment", not both. + retry_quotas: List of quotas that must be retried. By default retries only minutes quotas. + You must set this parameter to None, then you specify the 'retries' parameter as Retry instance. + You can specify quotas: + * MIN - Retry minutes quotas. + * HOUR - Retry hourly quotas. This is means that the program just sleeps for an hour! Be careful. + * DAY - Retry daily quotas. We strongly not recommended retrying these quotas. Example: How to create TolokaClient and make you first request to Toloka. @@ -185,7 +240,7 @@ class TolokaClient: SANDBOX = 'https://sandbox.toloka.yandex.com' PRODUCTION = 'https://toloka.yandex.com' - def __init__(self, token: str, environment: Union[Environment, str, None] = None, retries: Union[int, Retry] = 3, timeout: Union[float, Tuple[float, float]] = ..., url: Optional[str] = None): ... + def __init__(self, token: str, environment: Union[Environment, str, None] = None, retries: Union[int, Retry] = 3, timeout: Union[float, Tuple[float, float]] = ..., url: Optional[str] = None, retry_quotas: Union[List[str], str, None] = ...): ... def accept_assignment(self, assignment_id: str, public_comment: str) -> Assignment: """Marks one assignment as accepted diff --git a/src/client/__version__.py b/src/client/__version__.py index 410e5a4d..290b4935 100644 --- a/src/client/__version__.py +++ b/src/client/__version__.py @@ -1,3 +1,3 @@ __title__ = 'toloka-kit' -__version__ = '0.1.8' +__version__ = '0.1.9' __license__ = 'Apache 2.0' diff --git a/src/client/pool/__init__.py b/src/client/pool/__init__.py index ad880176..978575b7 100644 --- a/src/client/pool/__init__.py +++ b/src/client/pool/__init__.py @@ -1,4 +1,8 @@ __all__ = [ + 'dynamic_overlap_config', + 'dynamic_pricing_config', + 'mixer_config', + 'Pool', 'PoolPatchRequest', 'DynamicOverlapConfig', @@ -11,6 +15,10 @@ import attr +from . import dynamic_overlap_config +from . import dynamic_pricing_config +from . import mixer_config + from .dynamic_overlap_config import DynamicOverlapConfig from .dynamic_pricing_config import DynamicPricingConfig from .mixer_config import MixerConfig @@ -99,7 +107,7 @@ class Pool(BaseTolokaObject): >>> defaults=toloka.pool.Pool.Defaults(default_overlap_for_new_task_suites=3), >>> filter=toloka.filter.Languages.in_('EN'), >>> ) - >>> new_pool.set_mixer_config(real_tasks_count=10, golden_tasks_count=0, training_tasks_count=0) + >>> new_pool.set_mixer_config(real_tasks_count=10) >>> new_pool.quality_control.add_action(...) >>> new_pool = toloka_client.create_pool(new_pool) >>> print(new_pool.id) @@ -187,7 +195,7 @@ class Type(Enum): may_contain_adult_content: bool reward_per_assignment: float assignment_max_duration_seconds: int - defaults: Defaults + defaults: Defaults = attr.attrib(factory=lambda: Pool.Defaults(default_overlap_for_new_task_suites=1)) will_expire: datetime.datetime diff --git a/src/client/pool/__init__.pyi b/src/client/pool/__init__.pyi index 2d8bcdb7..4d72d05e 100644 --- a/src/client/pool/__init__.pyi +++ b/src/client/pool/__init__.pyi @@ -1,3 +1,15 @@ +__all__ = [ + 'dynamic_overlap_config', + 'dynamic_pricing_config', + 'mixer_config', + + 'Pool', + 'PoolPatchRequest', + 'DynamicOverlapConfig', + 'DynamicPricingConfig', + 'MixerConfig', +] + from datetime import datetime from enum import Enum from toloka.client.filter import FilterCondition @@ -92,7 +104,7 @@ class Pool(BaseTolokaObject): >>> defaults=toloka.pool.Pool.Defaults(default_overlap_for_new_task_suites=3), >>> filter=toloka.filter.Languages.in_('EN'), >>> ) - >>> new_pool.set_mixer_config(real_tasks_count=10, golden_tasks_count=0, training_tasks_count=0) + >>> new_pool.set_mixer_config(real_tasks_count=10) >>> new_pool.quality_control.add_action(...) >>> new_pool = toloka_client.create_pool(new_pool) >>> print(new_pool.id) @@ -279,7 +291,7 @@ class Pool(BaseTolokaObject): ... @overload - def set_mixer_config(self, *, real_tasks_count: Optional[int] = None, golden_tasks_count: Optional[int] = None, training_tasks_count: Optional[int] = None, min_real_tasks_count: Optional[int] = None, min_golden_tasks_count: Optional[int] = None, min_training_tasks_count: Optional[int] = None, force_last_assignment: Optional[bool] = None, force_last_assignment_delay_seconds: Optional[int] = None, mix_tasks_in_creation_order: Optional[bool] = None, shuffle_tasks_in_task_suite: Optional[bool] = None, golden_task_distribution_function: Optional[TaskDistributionFunction] = None, training_task_distribution_function: Optional[TaskDistributionFunction] = None): + def set_mixer_config(self, *, real_tasks_count: int = 0, golden_tasks_count: int = 0, training_tasks_count: int = 0, min_real_tasks_count: Optional[int] = None, min_golden_tasks_count: Optional[int] = None, min_training_tasks_count: Optional[int] = None, force_last_assignment: Optional[bool] = None, force_last_assignment_delay_seconds: Optional[int] = None, mix_tasks_in_creation_order: Optional[bool] = None, shuffle_tasks_in_task_suite: Optional[bool] = None, golden_task_distribution_function: Optional[TaskDistributionFunction] = None, training_task_distribution_function: Optional[TaskDistributionFunction] = None): """A shortcut setter for mixer_config """ ... diff --git a/src/client/pool/mixer_config.py b/src/client/pool/mixer_config.py index 9a6e076a..c4aa0c46 100644 --- a/src/client/pool/mixer_config.py +++ b/src/client/pool/mixer_config.py @@ -1,5 +1,6 @@ __all__ = ['MixerConfig'] -from ..primitives.base import BaseTolokaObject + +from ..primitives.base import attribute, BaseTolokaObject from ..task_distribution_function import TaskDistributionFunction @@ -47,9 +48,9 @@ class MixerConfig(BaseTolokaObject): change the frequency of training tasks as the user completes more tasks. """ - real_tasks_count: int - golden_tasks_count: int - training_tasks_count: int + real_tasks_count: int = attribute(default=0, required=True) + golden_tasks_count: int = attribute(default=0, required=True) + training_tasks_count: int = attribute(default=0, required=True) min_real_tasks_count: int min_golden_tasks_count: int min_training_tasks_count: int diff --git a/src/client/pool/mixer_config.pyi b/src/client/pool/mixer_config.pyi index d4998ecc..277865fa 100644 --- a/src/client/pool/mixer_config.pyi +++ b/src/client/pool/mixer_config.pyi @@ -56,9 +56,9 @@ class MixerConfig(BaseTolokaObject): ... _unexpected: Optional[Dict[str, Any]] - real_tasks_count: Optional[int] - golden_tasks_count: Optional[int] - training_tasks_count: Optional[int] + real_tasks_count: int + golden_tasks_count: int + training_tasks_count: int min_real_tasks_count: Optional[int] min_golden_tasks_count: Optional[int] min_training_tasks_count: Optional[int] diff --git a/src/client/primitives/operators.py b/src/client/primitives/operators.py index 2971f105..461aaa18 100644 --- a/src/client/primitives/operators.py +++ b/src/client/primitives/operators.py @@ -65,12 +65,15 @@ def _eq_compatible_with_help(cls, value): class _InclusionConditionMetaclass(BaseTolokaObjectMetaclass): - def in_(cls, value): + def include(cls, value): return cls(operator=InclusionOperator.IN, value=value) - def not_in(cls, value): + def exclude(cls, value): return cls(operator=InclusionOperator.NOT_IN, value=value) + in_ = include + not_in = exclude + __new__ = _create_operator_metaclass_new(InclusionOperator) diff --git a/src/client/primitives/retry.py b/src/client/primitives/retry.py new file mode 100644 index 00000000..bbdef74c --- /dev/null +++ b/src/client/primitives/retry.py @@ -0,0 +1,83 @@ +__all__ = [ + 'TolokaRetry', +] + +import json +import logging +from typing import Optional, List, Union +from urllib3.response import HTTPResponse # type: ignore +from urllib3.util.retry import Retry # type: ignore + + +logger = logging.getLogger(__name__) + + +class TolokaRetry(Retry): + """Retry toloka quotas. By default only minutes quotas. + + Args: + retry_quotas (Union[List[str], str, None]): List of quotas that will be retried. + None or empty list for not retrying quotas. + You can specify quotas: + * MIN - Retry minutes quotas. + * HOUR - Retry hourly quotas. This is means that the program just sleeps for an hour! Be careful. + * DAY - Retry daily quotas. We strongly not recommended retrying these quotas. + """ + class Unit: + MIN = 'MIN' + HOUR = 'HOUR' + DAY = 'DAY' + + seconds_to_wait = { + Unit.MIN: 60, + Unit.HOUR: 60*60, + Unit.DAY: 60*60*24, + } + + _retry_quotas: Union[List[str], str, None] = None + + def __init__(self, *args, retry_quotas: Union[List[str], str, None] = Unit.MIN, **kwargs): + if isinstance(retry_quotas, str): + self._retry_quotas = [retry_quotas] + else: + self._retry_quotas = retry_quotas + + self._last_response = kwargs.pop('last_response', None) + super(TolokaRetry, self).__init__(*args, **kwargs) + + def new(self, **kwargs): + kwargs['last_response'] = self._last_response + return super(TolokaRetry, self).new(retry_quotas=self._retry_quotas, **kwargs) + + def get_retry_after(self, response: "HTTPResponse") -> Optional[float]: + seconds = super(TolokaRetry, self).get_retry_after(response) + if seconds is not None: + return seconds + + if response.status != 429 or self._retry_quotas is None or self._last_response is None: + return None + payload = self._last_response.get('payload', None) + if payload is None or 'interval' not in payload: + return None + + interval = payload['interval'] + if interval not in self._retry_quotas: + return None + + if interval == TolokaRetry.Unit.HOUR: + logger.warning('The limit on hourly quotas worked. The program "falls asleep" for an hour.') + if interval == TolokaRetry.Unit.DAY: + logger.warning('The daily quota limit worked. The program "falls asleep" for the day.') + return TolokaRetry.seconds_to_wait.get(interval, None) + + def increment(self, *args, **kwargs) -> "Retry": + self._last_response = None + response = kwargs.get('response', None) + try: + if response is not None: + data = response.data + if data: + self._last_response = json.loads(response.data.decode("utf-8")) + except json.JSONDecodeError: + pass + return super(TolokaRetry, self).increment(*args, **kwargs) diff --git a/src/client/primitives/retry.pyi b/src/client/primitives/retry.pyi new file mode 100644 index 00000000..5315db91 --- /dev/null +++ b/src/client/primitives/retry.pyi @@ -0,0 +1,33 @@ +from typing import ( + List, + Optional, + Union +) +from urllib3.util.retry import Retry # type: ignore +from urllib3.response import HTTPResponse # type: ignore + + +class TolokaRetry(Retry): + """Retry toloka quotas. By default only minutes quotas. + + Args: + retry_quotas (Union[List[str], str, None]): List of quotas that will be retried. + None or empty list for not retrying quotas. + You can specify quotas: + * MIN - Retry minutes quotas. + * HOUR - Retry hourly quotas. This is means that the program just sleeps for an hour! Be careful. + * DAY - Retry daily quotas. We strongly not recommended retrying these quotas. + """ + + class Unit: + ... + + def __init__(self, *args, retry_quotas: Union[List[str], str, None] = 'MIN', **kwargs): ... + + def get_retry_after(self, response: HTTPResponse) -> Optional[float]: ... + + def increment(self, *args, **kwargs) -> 'Retry': ... + + def new(self, **kwargs): ... + + _retry_quotas: Union[List[str], str, None] diff --git a/src/client/project/__init__.py b/src/client/project/__init__.py index 879f7e5e..580dc30a 100644 --- a/src/client/project/__init__.py +++ b/src/client/project/__init__.py @@ -1,4 +1,9 @@ __all__ = [ + 'field_spec', + 'task_spec', + 'template_builder', + 'view_spec', + 'Project', 'ClassicViewSpec', 'TemplateBuilderViewSpec', @@ -22,7 +27,12 @@ import datetime from enum import Enum, unique -from ..primitives.base import BaseTolokaObject +from . import field_spec +from . import task_spec +from . import template_builder +from . import view_spec + +from ..primitives.base import attribute, BaseTolokaObject from ..project.field_spec import ( BooleanSpec, StringSpec, @@ -82,7 +92,6 @@ class Project(BaseTolokaObject): >>> toloka_client = toloka.TolokaClient(your_token, 'PRODUCTION') >>> new_project = toloka.project.Project( - >>> assignments_issuing_type=toloka.project.Project.AssignmentsIssuingType.AUTOMATED, >>> public_name='My best project!!!', >>> public_description='Look at the instruction and do it well', >>> public_instructions='!Describe your task for performers here!', @@ -141,7 +150,7 @@ class AssignmentsIssuingViewConfig(BaseTolokaObject): public_name: str # public public_description: str # public task_spec: TaskSpec # public - assignments_issuing_type: AssignmentsIssuingType # AssignmentsIssuingType # public + assignments_issuing_type: AssignmentsIssuingType = attribute(default=AssignmentsIssuingType.AUTOMATED, required=True) # AssignmentsIssuingType # public assignments_issuing_view_config: AssignmentsIssuingViewConfig assignments_automerge_enabled: bool diff --git a/src/client/project/__init__.pyi b/src/client/project/__init__.pyi index e2210f24..c321aa0d 100644 --- a/src/client/project/__init__.pyi +++ b/src/client/project/__init__.pyi @@ -1,3 +1,29 @@ +__all__ = [ + 'field_spec', + 'task_spec', + 'template_builder', + 'view_spec', + + 'Project', + 'ClassicViewSpec', + 'TemplateBuilderViewSpec', + 'BooleanSpec', + 'StringSpec', + 'IntegerSpec', + 'FloatSpec', + 'UrlSpec', + 'FileSpec', + 'CoordinatesSpec', + 'JsonSpec', + 'ArrayBooleanSpec', + 'ArrayStringSpec', + 'ArrayIntegerSpec', + 'ArrayFloatSpec', + 'ArrayUrlSpec', + 'ArrayFileSpec', + 'ArrayCoordinatesSpec', +] + from datetime import datetime from enum import Enum from toloka.client.primitives.base import BaseTolokaObject @@ -67,7 +93,6 @@ class Project(BaseTolokaObject): >>> toloka_client = toloka.TolokaClient(your_token, 'PRODUCTION') >>> new_project = toloka.project.Project( - >>> assignments_issuing_type=toloka.project.Project.AssignmentsIssuingType.AUTOMATED, >>> public_name='My best project!!!', >>> public_description='Look at the instruction and do it well', >>> public_instructions='!Describe your task for performers here!', @@ -134,7 +159,7 @@ class Project(BaseTolokaObject): public_name: Optional[str] public_description: Optional[str] task_spec: Optional[TaskSpec] - assignments_issuing_type: Optional[AssignmentsIssuingType] + assignments_issuing_type: AssignmentsIssuingType assignments_issuing_view_config: Optional[AssignmentsIssuingViewConfig] assignments_automerge_enabled: Optional[bool] max_active_assignments_count: Optional[int] diff --git a/src/client/project/template_builder/__init__.py b/src/client/project/template_builder/__init__.py index d932c82c..b94bc0d8 100644 --- a/src/client/project/template_builder/__init__.py +++ b/src/client/project/template_builder/__init__.py @@ -1,4 +1,15 @@ __all__ = [ + + 'actions', + 'base', + 'conditions', + 'data', + 'fields', + 'helpers', + 'layouts', + 'plugins', + 'view', + 'TemplateBuilder', 'get_input_and_output', 'BulkActionV1', @@ -76,8 +87,19 @@ 'TextViewV1', 'VideoViewV1', ] + from typing import Dict, List, Any, Union, Tuple +from . import actions +from . import base +from . import conditions +from . import data +from . import fields +from . import helpers +from . import layouts +from . import plugins +from . import view + from .actions import ( BulkActionV1, NotifyActionV1, diff --git a/src/client/project/template_builder/__init__.pyi b/src/client/project/template_builder/__init__.pyi index 53364980..5af714fc 100644 --- a/src/client/project/template_builder/__init__.pyi +++ b/src/client/project/template_builder/__init__.pyi @@ -1,3 +1,92 @@ +__all__ = [ + + 'actions', + 'conditions', + 'data', + 'fields', + 'helpers', + 'layouts', + 'plugins', + 'view', + + 'TemplateBuilder', + 'get_input_and_output', + 'BulkActionV1', + 'NotifyActionV1', + 'OpenCloseActionV1', + 'OpenLinkActionV1', + 'PlayPauseActionV1', + 'RotateActionV1', + 'SetActionV1', + 'ToggleActionV1', + 'AllConditionV1', + 'AnyConditionV1', + 'EmptyConditionV1', + 'EqualsConditionV1', + 'LinkOpenedConditionV1', + 'NotConditionV1', + 'PlayedConditionV1', + 'PlayedFullyConditionV1', + 'RequiredConditionV1', + 'SchemaConditionV1', + 'SubArrayConditionV1', + 'InputData', + 'InternalData', + 'LocalData', + 'OutputData', + 'RelativeData', + 'ButtonRadioFieldV1', + 'GroupFieldOption', + 'ButtonRadioGroupFieldV1', + 'CheckboxFieldV1', + 'CheckboxGroupFieldV1', + 'DateFieldV1', + 'EmailFieldV1', + 'FileFieldV1', + 'ImageAnnotationFieldV1', + 'ListFieldV1', + 'MediaFileFieldV1', + 'NumberFieldV1', + 'PhoneNumberFieldV1', + 'RadioGroupFieldV1', + 'SelectFieldV1', + 'TextFieldV1', + 'TextareaFieldV1', + 'ConcatArraysHelperV1', + 'Entries2ObjectHelperV1', + 'IfHelperV1', + 'JoinHelperV1', + 'Object2EntriesHelperV1', + 'ReplaceHelperV1', + 'SearchQueryHelperV1', + 'SwitchHelperV1', + 'TextTransformHelperV1', + 'TransformHelperV1', + 'YandexDiskProxyHelperV1', + 'BarsLayoutV1', + 'ColumnsLayoutV1', + 'SideBySideLayoutV1', + 'SidebarLayoutV1', + 'HotkeysPluginV1', + 'TriggerPluginV1', + 'TolokaPluginV1', + 'ActionButtonViewV1', + 'AlertViewV1', + 'AudioViewV1', + 'CollapseViewV1', + 'DeviceFrameViewV1', + 'DividerViewV1', + 'GroupViewV1', + 'IframeViewV1', + 'ImageViewV1', + 'LabeledListViewV1', + 'LinkViewV1', + 'ListViewV1', + 'MarkdownViewV1', + 'TextViewV1', + 'VideoViewV1', +] + from toloka.client.primitives.base import BaseTolokaObject from toloka.client.project.field_spec import FieldSpec from toloka.client.project.template_builder.actions import ( diff --git a/src/client/project/template_builder/plugins.py b/src/client/project/template_builder/plugins.py index 2d70a24e..40729846 100644 --- a/src/client/project/template_builder/plugins.py +++ b/src/client/project/template_builder/plugins.py @@ -2,12 +2,14 @@ 'BasePluginV1', 'HotkeysPluginV1', 'TriggerPluginV1', - 'TolokaPluginV1' + 'TolokaPluginV1', ] + from enum import Enum, unique from typing import List, Any from ...primitives.base import attribute +from ...util._codegen import expand from .base import VersionedBaseComponent, BaseComponent, ComponentType, BaseTemplate, base_component_or @@ -123,10 +125,8 @@ class TolokaPluginV1(BasePluginV1, spec_value=ComponentType.PLUGIN_TOLOKA): How to set the task width on the task page. >>> task_width_plugin = tb.plugins.TolokaPluginV1( - >>> layout = tb.plugins.TolokaPluginV1.TolokaPluginLayout( - >>> kind='scroll', - >>> task_width=400, - >>> ) + >>> kind='scroll', + >>> task_width=400, >>> ) ... """ @@ -148,8 +148,10 @@ class Kind(Enum): PAGER = 'pager' SCROLL = 'scroll' - kind: Kind = Kind.SCROLL + kind: Kind task_width: float = attribute(origin='taskWidth') layout: base_component_or(TolokaPluginLayout) = attribute(factory=TolokaPluginLayout) notifications: base_component_or(List[BaseComponent], 'ListBaseComponent') + +TolokaPluginV1.__init__ = expand('layout', TolokaPluginV1.TolokaPluginLayout)(TolokaPluginV1.__init__) diff --git a/src/client/project/template_builder/plugins.pyi b/src/client/project/template_builder/plugins.pyi index 6be2ab89..a97d61d1 100644 --- a/src/client/project/template_builder/plugins.pyi +++ b/src/client/project/template_builder/plugins.pyi @@ -9,7 +9,8 @@ from typing import ( Dict, List, Optional, - Union + Union, + overload, ) class BasePluginV1(VersionedBaseComponent): @@ -135,6 +136,8 @@ class TolokaPluginV1(BasePluginV1): """A plugin with extra settings for tasks in Toloka. Attributes: + kind: Layout direction. + task_width: Width of the block with the task. By default, the task is displayed at full width. layout: Settings for the task appearance in Toloka. notifications: Notifications shown at the top of the page. @@ -152,6 +155,11 @@ class TolokaPluginV1(BasePluginV1): class TolokaPluginLayout(BaseTemplate): """How to display task. + + Attributes: + kind: Layout direction. + task_width: Width of the block with the task. By default, the task is displayed at full width. + """ class Kind(Enum): @@ -174,6 +182,13 @@ class TolokaPluginV1(BasePluginV1): kind: Optional[Kind] task_width: Optional[float] + @overload + def __init__(self, *, version: Optional[str] = '1.0.0', kind: Optional[TolokaPluginLayout.Kind] = None, task_width: Optional[float] = None, notifications: Optional[Union[BaseComponent, List[BaseComponent]]] = None) -> None: + """Method generated by attrs for class TolokaPluginV1. + """ + ... + + @overload def __init__(self, *, version: Optional[str] = '1.0.0', layout: Optional[Union[BaseComponent, TolokaPluginLayout]] = ..., notifications: Optional[Union[BaseComponent, List[BaseComponent]]] = None) -> None: """Method generated by attrs for class TolokaPluginV1. """ diff --git a/src/client/project/view_spec.py b/src/client/project/view_spec.py index c02ba2af..e90a5932 100644 --- a/src/client/project/view_spec.py +++ b/src/client/project/view_spec.py @@ -13,6 +13,7 @@ from .template_builder import TemplateBuilder from ..primitives.base import attribute, BaseTolokaObject from ..util import traverse_dicts_recursively +from ..util._codegen import expand class ViewSpec(BaseTolokaObject, spec_enum='Type', spec_field='type'): @@ -117,7 +118,9 @@ class TemplateBuilderViewSpec(ViewSpec, spec_value=ViewSpec.TEMPLATE_BUILDER): template builder components Attributes: - config: A template builder config + view: + plugins: + vars: core_version: Default template components version. Most users will not need to change this parameter. Example: @@ -125,13 +128,11 @@ class TemplateBuilderViewSpec(ViewSpec, spec_value=ViewSpec.TEMPLATE_BUILDER): >>> import toloka.client.project.template_builder as tb >>> project_interface = toloka.project.view_spec.TemplateBuilderViewSpec( - >>> config=tb.TemplateBuilder( - >>> view=tb.view.ListViewV1( - >>> items=[header, output_field, radiobuttons], - >>> validation=some_validation, - >>> ), - >>> plugins=[plugin1, plugin2] - >>> ) + >>> view=tb.view.ListViewV1( + >>> items=[header, output_field, radiobuttons], + >>> validation=some_validation, + >>> ), + >>> plugins=[plugin1, plugin2] >>> ) >>> # add 'project_interface' to 'toloka.project.Project' instance ... @@ -157,7 +158,7 @@ def unstructure(self): raise RuntimeError(f'Different versions of the same component: {comp_type}') data['lock'] = lock - data['config'] = json.dumps(data['config']) + data['config'] = json.dumps(data['config'], indent=4, ensure_ascii=False) return data @classmethod @@ -173,3 +174,6 @@ def structure(cls, data: dict): dct['version'] = lock[dct['type']] return super().structure(data_copy) + + +TemplateBuilderViewSpec.__init__ = expand('config')(TemplateBuilderViewSpec.__init__) diff --git a/src/client/project/view_spec.pyi b/src/client/project/view_spec.pyi index 593f8582..a6000e8b 100644 --- a/src/client/project/view_spec.pyi +++ b/src/client/project/view_spec.pyi @@ -1,6 +1,8 @@ from enum import Enum from toloka.client.primitives.base import BaseTolokaObject from toloka.client.project.template_builder import TemplateBuilder +from toloka.client.project.template_builder.base import BaseComponent + from typing import ( Any, Dict, @@ -130,19 +132,17 @@ class TemplateBuilderViewSpec(ViewSpec): >>> import toloka.client.project.template_builder as tb >>> project_interface = toloka.project.view_spec.TemplateBuilderViewSpec( - >>> config=tb.TemplateBuilder( - >>> view=tb.view.ListViewV1( - >>> items=[header, output_field, radiobuttons], - >>> validation=some_validation, - >>> ), - >>> plugins=[plugin1, plugin2] - >>> ) + >>> view=tb.view.ListViewV1( + >>> items=[header, output_field, radiobuttons], + >>> validation=some_validation, + >>> ), + >>> plugins=[plugin1, plugin2] >>> ) >>> # add 'project_interface' to 'toloka.project.Project' instance ... """ - def __init__(self, *, settings: Optional[ViewSpec.Settings] = None, config: Optional[TemplateBuilder] = None, core_version: Optional[str] = '1.0.0') -> None: + def __init__(self, *, settings: Optional[ViewSpec.Settings] = None, view: Optional[BaseComponent] = None, plugins: Optional[List[BaseComponent]] = None, vars: Optional[Dict[str, Any]] = None, core_version: Optional[str] = '1.0.0') -> None: """Method generated by attrs for class TemplateBuilderViewSpec. """ ... diff --git a/src/client/util/_codegen.py b/src/client/util/_codegen.py index 5459972f..7548880f 100644 --- a/src/client/util/_codegen.py +++ b/src/client/util/_codegen.py @@ -4,7 +4,7 @@ import uuid from inspect import signature, Signature, Parameter from textwrap import dedent, indent -from typing import Callable, Dict, List, Optional +from typing import Callable, Dict, List, Optional, Type import attr @@ -154,13 +154,14 @@ def codegen_attr_attributes_setters(cls): return cls -def expand_func_by_argument(func: Callable, arg_name: str) -> Callable: +def expand_func_by_argument(func: Callable, arg_name: str, arg_type: Optional[Type] = None) -> Callable: func_sig = _get_signature(func) func_params = list(func_sig.parameters.values()) arg_param = func_sig.parameters[arg_name] arg_index = next(i for (i, p) in enumerate(func_params) if p is arg_param) - arg_type = is_optional_of(arg_param.annotation) or arg_param.annotation + if arg_type is None: + arg_type = is_optional_of(arg_param.annotation) or arg_param.annotation arg_type_sig = _get_signature(arg_type) # TODO: add tests @@ -183,11 +184,11 @@ def expand_func_by_argument(func: Callable, arg_name: str) -> Callable: return expanded_func -def expand(arg_name): +def expand(arg_name, arg_type=None): def wrapper(func): func_sig = _get_signature(func) - expanded_func = expand_func_by_argument(func, arg_name) + expanded_func = expand_func_by_argument(func, arg_name, arg_type) @functools.wraps(func) def wrapped(*args, **kwargs): diff --git a/tests/test_retries.py b/tests/test_retries.py index 4367d3ef..7ac6a5a6 100644 --- a/tests/test_retries.py +++ b/tests/test_retries.py @@ -32,7 +32,7 @@ def test_retries_from_int(): def test_retries_from_class(): with mock.patch('urllib3.connection.HTTPSConnection.request') as request_mock: request_mock.side_effect = urllib_request_mock - toloka_client = TolokaClient('fake-token', 'SANDBOX', Retry(connect=2)) + toloka_client = TolokaClient('fake-token', 'SANDBOX', Retry(connect=2), retry_quotas=None) with pytest.raises(requests.exceptions.ConnectionError): toloka_client.get_requester() assert request_mock.call_count == 3