From 28c0395349b5cb63f62db56d9043a895e3503b43 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Thu, 1 Dec 2022 16:44:40 -0800 Subject: [PATCH 01/42] Add Weaviate Destination #20012 --- .../destination-weaviate/.dockerignore | 5 + .../destination-weaviate/Dockerfile | 38 +++++ .../connectors/destination-weaviate/README.md | 123 +++++++++++++++ .../destination-weaviate/build.gradle | 8 + .../destination_weaviate/__init__.py | 8 + .../destination_weaviate/client.py | 41 +++++ .../destination_weaviate/destination.py | 78 ++++++++++ .../destination_weaviate/spec.json | 39 +++++ .../destination_weaviate/writer.py | 0 .../integration_tests/example-config.json | 1 + .../integration_tests/integration_test.py | 143 ++++++++++++++++++ .../connectors/destination-weaviate/main.py | 11 ++ .../destination-weaviate/requirements.txt | 1 + .../connectors/destination-weaviate/setup.py | 29 ++++ .../unit_tests/unit_test.py | 7 + 15 files changed, 532 insertions(+) create mode 100644 airbyte-integrations/connectors/destination-weaviate/.dockerignore create mode 100644 airbyte-integrations/connectors/destination-weaviate/Dockerfile create mode 100644 airbyte-integrations/connectors/destination-weaviate/README.md create mode 100644 airbyte-integrations/connectors/destination-weaviate/build.gradle create mode 100644 airbyte-integrations/connectors/destination-weaviate/destination_weaviate/__init__.py create mode 100644 airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py create mode 100644 airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py create mode 100644 airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json create mode 100644 airbyte-integrations/connectors/destination-weaviate/destination_weaviate/writer.py create mode 100644 airbyte-integrations/connectors/destination-weaviate/integration_tests/example-config.json create mode 100644 airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py create mode 100644 airbyte-integrations/connectors/destination-weaviate/main.py create mode 100644 airbyte-integrations/connectors/destination-weaviate/requirements.txt create mode 100644 airbyte-integrations/connectors/destination-weaviate/setup.py create mode 100644 airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py diff --git a/airbyte-integrations/connectors/destination-weaviate/.dockerignore b/airbyte-integrations/connectors/destination-weaviate/.dockerignore new file mode 100644 index 000000000000..0c91a8221067 --- /dev/null +++ b/airbyte-integrations/connectors/destination-weaviate/.dockerignore @@ -0,0 +1,5 @@ +* +!Dockerfile +!main.py +!destination_weaviate +!setup.py diff --git a/airbyte-integrations/connectors/destination-weaviate/Dockerfile b/airbyte-integrations/connectors/destination-weaviate/Dockerfile new file mode 100644 index 000000000000..a53ba2f1eb22 --- /dev/null +++ b/airbyte-integrations/connectors/destination-weaviate/Dockerfile @@ -0,0 +1,38 @@ +FROM python:3.9.11-alpine3.15 as base + +# build and load all requirements +FROM base as builder +WORKDIR /airbyte/integration_code + +# upgrade pip to the latest version +RUN apk --no-cache upgrade \ + && pip install --upgrade pip \ + && apk --no-cache add tzdata build-base + + +COPY setup.py ./ +# install necessary packages to a temporary folder +RUN pip install --prefix=/install . + +# build a clean environment +FROM base +WORKDIR /airbyte/integration_code + +# copy all loaded and built libraries to a pure basic image +COPY --from=builder /install /usr/local +# add default timezone settings +COPY --from=builder /usr/share/zoneinfo/Etc/UTC /etc/localtime +RUN echo "Etc/UTC" > /etc/timezone + +# bash is installed for more convenient debugging. +RUN apk --no-cache add bash + +# copy payload code only +COPY main.py ./ +COPY destination_weaviate ./destination_weaviate + +ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" +ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] + +LABEL io.airbyte.version=0.1.0 +LABEL io.airbyte.name=airbyte/destination-weaviate diff --git a/airbyte-integrations/connectors/destination-weaviate/README.md b/airbyte-integrations/connectors/destination-weaviate/README.md new file mode 100644 index 000000000000..ff12caf64f32 --- /dev/null +++ b/airbyte-integrations/connectors/destination-weaviate/README.md @@ -0,0 +1,123 @@ +# Weaviate Destination + +This is the repository for the Weaviate destination connector, written in Python. +For information about how to use this connector within Airbyte, see [the documentation](https://docs.airbyte.io/integrations/destinations/weaviate). + +## Local development + +### Prerequisites +**To iterate on this connector, make sure to complete this prerequisites section.** + +#### Minimum Python version required `= 3.7.0` + +#### Build & Activate Virtual Environment and install dependencies +From this connector directory, create a virtual environment: +``` +python -m venv .venv +``` + +This will generate a virtualenv for this module in `.venv/`. Make sure this venv is active in your +development environment of choice. To activate it from the terminal, run: +``` +source .venv/bin/activate +pip install -r requirements.txt +``` +If you are in an IDE, follow your IDE's instructions to activate the virtualenv. + +Note that while we are installing dependencies from `requirements.txt`, you should only edit `setup.py` for your dependencies. `requirements.txt` is +used for editable installs (`pip install -e`) to pull in Python dependencies from the monorepo and will call `setup.py`. +If this is mumbo jumbo to you, don't worry about it, just put your deps in `setup.py` but install using `pip install -r requirements.txt` and everything +should work as you expect. + +#### Building via Gradle +From the Airbyte repository root, run: +``` +./gradlew :airbyte-integrations:connectors:destination-weaviate:build +``` + +#### Create credentials +**If you are a community contributor**, follow the instructions in the [documentation](https://docs.airbyte.io/integrations/destinations/weaviate) +to generate the necessary credentials. Then create a file `secrets/config.json` conforming to the `destination_weaviate/spec.json` file. +Note that the `secrets` directory is gitignored by default, so there is no danger of accidentally checking in sensitive information. +See `integration_tests/sample_config.json` for a sample config file. + +**If you are an Airbyte core member**, copy the credentials in Lastpass under the secret name `destination weaviate test creds` +and place them into `secrets/config.json`. + +### Locally running the connector +``` +python main.py spec +python main.py check --config secrets/config.json +python main.py discover --config secrets/config.json +python main.py read --config secrets/config.json --catalog integration_tests/configured_catalog.json +``` + +### Locally running the connector docker image + +#### Build +First, make sure you build the latest Docker image: +``` +docker build . -t airbyte/destination-weaviate:dev +``` + +You can also build the connector image via Gradle: +``` +./gradlew :airbyte-integrations:connectors:destination-weaviate:airbyteDocker +``` +When building via Gradle, the docker image name and tag, respectively, are the values of the `io.airbyte.name` and `io.airbyte.version` `LABEL`s in +the Dockerfile. + +#### Run +Then run any of the connector commands as follows: +``` +docker run --rm airbyte/destination-weaviate:dev spec +docker run --rm -v $(pwd)/secrets:/secrets airbyte/destination-weaviate:dev check --config /secrets/config.json +# messages.jsonl is a file containing line-separated JSON representing AirbyteMessages +cat messages.jsonl | docker run --rm -v $(pwd)/secrets:/secrets -v $(pwd)/integration_tests:/integration_tests airbyte/destination-weaviate:dev write --config /secrets/config.json --catalog /integration_tests/configured_catalog.json +``` +## Testing + Make sure to familiarize yourself with [pytest test discovery](https://docs.pytest.org/en/latest/goodpractices.html#test-discovery) to know how your test files and methods should be named. +First install test dependencies into your virtual environment: +``` +pip install .[tests] +``` +### Unit Tests +To run unit tests locally, from the connector directory run: +``` +python -m pytest unit_tests +``` + +### Integration Tests +There are two types of integration tests: Acceptance Tests (Airbyte's test suite for all destination connectors) and custom integration tests (which are specific to this connector). +#### Custom Integration tests +Place custom tests inside `integration_tests/` folder, then, from the connector root, run +``` +python -m pytest integration_tests +``` +#### Acceptance Tests +Coming soon: + +### Using gradle to run tests +All commands should be run from airbyte project root. +To run unit tests: +``` +./gradlew :airbyte-integrations:connectors:destination-weaviate:unitTest +``` +To run acceptance and custom integration tests: +``` +./gradlew :airbyte-integrations:connectors:destination-weaviate:integrationTest +``` + +## Dependency Management +All of your dependencies should go in `setup.py`, NOT `requirements.txt`. The requirements file is only used to connect internal Airbyte dependencies in the monorepo for local development. +We split dependencies between two groups, dependencies that are: +* required for your connector to work need to go to `MAIN_REQUIREMENTS` list. +* required for the testing need to go to `TEST_REQUIREMENTS` list + +### Publishing a new version of the connector +You've checked out the repo, implemented a million dollar feature, and you're ready to share your changes with the world. Now what? +1. Make sure your changes are passing unit and integration tests. +1. Bump the connector version in `Dockerfile` -- just increment the value of the `LABEL io.airbyte.version` appropriately (we use [SemVer](https://semver.org/)). +1. Create a Pull Request. +1. Pat yourself on the back for being an awesome contributor. +1. Someone from Airbyte will take a look at your PR and iterate with you to merge it into master. diff --git a/airbyte-integrations/connectors/destination-weaviate/build.gradle b/airbyte-integrations/connectors/destination-weaviate/build.gradle new file mode 100644 index 000000000000..b2101bb02449 --- /dev/null +++ b/airbyte-integrations/connectors/destination-weaviate/build.gradle @@ -0,0 +1,8 @@ +plugins { + id 'airbyte-python' + id 'airbyte-docker' +} + +airbytePython { + moduleDirectory 'destination_weaviate' +} diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/__init__.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/__init__.py new file mode 100644 index 000000000000..9b541415f151 --- /dev/null +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/__init__.py @@ -0,0 +1,8 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# + + +from .destination import DestinationWeaviate + +__all__ = ["DestinationWeaviate"] diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py new file mode 100644 index 000000000000..c77f36704dd2 --- /dev/null +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -0,0 +1,41 @@ +import weaviate + +from typing import Any, Mapping +import uuid + + +class Client: + def __init__(self, config: Mapping[str, Any]): + self.client = self.get_weaviate_client(config) + self.config = config + self.batch_size = 100 + + def queue_write_operation(self, stream_name: str, record: Mapping): + # TODO need to handle case where original DB ID is not a UUID + id = "" + if record.get('id'): + id = record.get("id") + del record["id"] + else: + id = uuid.uuid4() + + self.client.batch.add_data_object(record, stream_name, id) + if self.client.batch.num_objects() >= self.batch_size: + self.client.batch.create_objects() + + def flush(self): + self.client.batch.create_objects() + + @staticmethod + def get_weaviate_client(config: Mapping[str, Any]) -> weaviate.Client: + url, username, password = config.get("url"), config.get("username"), config.get("password") + + if username and not password: + raise Exception("Password is required when username is set") + if password and not username: + raise Exception("Username is required when password is set") + + if username and password: + credentials = weaviate.auth.AuthClientPassword(username, password) + return weaviate.Client(url=url, auth_client_secret=credentials) + return weaviate.Client(url=url, timeout_config=(2, 2)) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py new file mode 100644 index 000000000000..457966074141 --- /dev/null +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py @@ -0,0 +1,78 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# + + +from typing import Any, Iterable, Mapping + +from airbyte_cdk import AirbyteLogger +from airbyte_cdk.destinations import Destination +from airbyte_cdk.models import AirbyteConnectionStatus, AirbyteMessage, ConfiguredAirbyteCatalog, DestinationSyncMode, Status, Type + + +from .client import Client + + +class DestinationWeaviate(Destination): + def write( + self, config: Mapping[str, Any], configured_catalog: ConfiguredAirbyteCatalog, input_messages: Iterable[AirbyteMessage] + ) -> Iterable[AirbyteMessage]: + + """ + TODO + Reads the input stream of messages, config, and catalog to write data to the destination. + + This method returns an iterable (typically a generator of AirbyteMessages via yield) containing state messages received + in the input message stream. Outputting a state message means that every AirbyteRecordMessage which came before it has been + successfully persisted to the destination. This is used to ensure fault tolerance in the case that a sync fails before fully completing, + then the source is given the last state message output from this method as the starting point of the next sync. + + :param config: dict of JSON configuration matching the configuration declared in spec.json + :param configured_catalog: The Configured Catalog describing the schema of the data being received and how it should be persisted in the + destination + :param input_messages: The stream of input messages received from the source + :return: Iterable of AirbyteStateMessages wrapped in AirbyteMessage structs + """ + client = Client(config) + # TODO add support for overwrite mode + #for configured_stream in configured_catalog.streams: + # if configured_stream.destination_sync_mode == DestinationSyncMode.overwrite: + # client.delete_stream_entries(configured_stream.stream.name) + + for message in input_messages: + if message.type == Type.STATE: + # Emitting a state message indicates that all records which came before it have been written to the destination. So we flush + # the queue to ensure writes happen, then output the state message to indicate it's safe to checkpoint state + client.flush() + yield message + elif message.type == Type.RECORD: + record = message.record + client.queue_write_operation(record.stream, record.data) + else: + # ignore other message types for now + continue + + # Make sure to flush any records still in the queue + client.flush() + + def check(self, logger: AirbyteLogger, config: Mapping[str, Any]) -> AirbyteConnectionStatus: + """ + Tests if the input configuration can be used to successfully connect to the destination with the needed permissions + e.g: if a provided API token or password can be used to connect and write to the destination. + + :param logger: Logging object to display debug/info/error to the logs + (logs will not be accessible via airbyte UI if they are not passed to this logger) + :param config: Json object containing the configuration of this destination, content of this json is as specified in + the properties of the spec.json file + + :return: AirbyteConnectionStatus indicating a Success or Failure + """ + try: + client = Client.get_weaviate_client(config) + ready = client.is_ready() + if not ready: + return AirbyteConnectionStatus(status=Status.FAILED, + message=f"Weaviate server {config.get('url')} not ready") + return AirbyteConnectionStatus(status=Status.SUCCEEDED) + except Exception as e: + return AirbyteConnectionStatus(status=Status.FAILED, message=f"An exception occurred: {repr(e)}") diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json new file mode 100644 index 000000000000..bb5af766f33a --- /dev/null +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json @@ -0,0 +1,39 @@ +{ + "documentationUrl" : "https://docs.airbyte.com/integrations/destinations/weaviate", + "supported_destination_sync_modes" : [ + "append" + ], + "supportsIncremental" : true, + "supportsDBT" : false, + "supportsNormalization" : false, + "connectionSpecification" : { + "$schema" : "http://json-schema.org/draft-07/schema#", + "title" : "Destination Weaviate", + "type" : "object", + "required" : [ + "url" + ], + "additionalProperties" : false, + "properties" : { + "url" : { + "type" : "string", + "description" : "The URL to the weaviate instance", + "examples" : [ + "http://localhost:8080", + "https://your-instance.semi.network" + ] + }, + "username" : { + "type" : "string", + "description" : "Username used with OIDC authentication", + "examples" : [ + "xyz@weaviate.io" + ] + }, + "password" : { + "type" : "string", + "description" : "Password used with OIDC authentication" + } + } + } +} diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/writer.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/writer.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/example-config.json b/airbyte-integrations/connectors/destination-weaviate/integration_tests/example-config.json new file mode 100644 index 000000000000..cc10c7772dc2 --- /dev/null +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/example-config.json @@ -0,0 +1 @@ +{ "url": "http://localhost:8080"} diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py new file mode 100644 index 000000000000..6e3c523ec5ab --- /dev/null +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -0,0 +1,143 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# + +import json +from typing import Any, Dict, List, Mapping +import time +import logging + +import pytest +from airbyte_cdk import AirbyteLogger +from airbyte_cdk.models import ( + AirbyteMessage, + AirbyteRecordMessage, + AirbyteStateMessage, + AirbyteStream, + ConfiguredAirbyteCatalog, + ConfiguredAirbyteStream, + DestinationSyncMode, + Status, + SyncMode, + Type, +) +import docker +from destination_weaviate import DestinationWeaviate +from destination_weaviate.client import Client + + +@pytest.fixture(name="config") +def config_fixture() -> Mapping[str, Any]: + with open("integration_tests/example-config.json", "r") as f: + return json.loads(f.read()) + + +@pytest.fixture(name="configured_catalog") +def configured_catalog_fixture() -> ConfiguredAirbyteCatalog: + stream_schema = {"type": "object", "properties": {"title": {"type": "str"}, "wordCount": {"type": "integer"}}} + + append_stream = ConfiguredAirbyteStream( + stream=AirbyteStream(name="Article", json_schema=stream_schema, supported_sync_modes=[SyncMode.incremental]), + sync_mode=SyncMode.incremental, + destination_sync_mode=DestinationSyncMode.append, + ) + + return ConfiguredAirbyteCatalog(streams=[append_stream]) + + +@pytest.fixture(autouse=True) +def setup_teardown(config: Mapping): + env_vars = { + "QUERY_DEFAULTS_LIMIT": "25", + "AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED": "true", + "DEFAULT_VECTORIZER_MODULE": "none", + "CLUSTER_HOSTNAME": "node1", + "PERSISTENCE_DATA_PATH": "./data" + } + name = "weaviate-test-container-will-get-deleted" + docker_client = docker.from_env() + try: + docker_client.containers.get(name).remove(force=True) + except docker.errors.NotFound: + pass + + docker_client.containers.run( + "semitechnologies/weaviate:1.16.1", detach=True, environment=env_vars, name=name, + ports={8080: ('127.0.0.1', 8080)} + ) + + retries = 3 + client = None + while retries > 0: + try: + client = Client(config) + break + except Exception as e: + logging.info(f"error connecting to weaviate with client. Retrying in 1 second. Exception: {e}") + time.sleep(1) + retries -= 1 + + yield + docker_client.containers.get(name).remove(force=True) + + +@pytest.fixture(name="client") +def client_fixture(config) -> Client: + return Client(config) + + +def test_check_valid_config(config: Mapping): + outcome = DestinationWeaviate().check(AirbyteLogger(), config) + assert outcome.status == Status.SUCCEEDED + + +def test_check_invalid_config(): + outcome = DestinationWeaviate().check(AirbyteLogger(), {"url": "localhost:6666"}) + assert outcome.status == Status.FAILED + + +def _state(data: Dict[str, Any]) -> AirbyteMessage: + return AirbyteMessage(type=Type.STATE, state=AirbyteStateMessage(data=data)) + + +def _record(stream: str, title: str, word_count: int) -> AirbyteMessage: + return AirbyteMessage( + type=Type.RECORD, record=AirbyteRecordMessage(stream=stream, data={"title": title, "wordCount": word_count}, emitted_at=0) + ) + + +def retrieve_all_records(client: Client) -> List[AirbyteRecordMessage]: + """retrieves and formats all Articles as Airbyte messages""" + all_records = client.client.data_object.get(class_name="Article") + out = [] + for record in all_records.get("objects"): + props = record["properties"] + out.append(_record("Article", props["title"], props["wordCount"])) + out.sort(key=lambda x: x.record.data.get("title")) + return out + + +def test_write(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, client: Client): + """ + This test verifies that: + TODO: 1. writing a stream in "overwrite" mode overwrites any existing data for that stream + 2. writing a stream in "append" mode appends new records without deleting the old ones + 3. The correct state message is output by the connector at the end of the sync + """ + append_stream = configured_catalog.streams[0].stream.name + first_state_message = _state({"state": "1"}) + first_record_chunk = [_record(append_stream, str(i), i) for i in range(5)] + + destination = DestinationWeaviate() + + expected_states = [first_state_message] + output_states = list( + destination.write( + config, configured_catalog, [*first_record_chunk, first_state_message] + ) + ) + assert expected_states == output_states, "Checkpoint state messages were expected from the destination" + + expected_records = [_record(append_stream, str(i), i) for i in range(5)] + records_in_destination = retrieve_all_records(client) + assert expected_records == records_in_destination, "Records in destination should match records expected" diff --git a/airbyte-integrations/connectors/destination-weaviate/main.py b/airbyte-integrations/connectors/destination-weaviate/main.py new file mode 100644 index 000000000000..f303b9286acd --- /dev/null +++ b/airbyte-integrations/connectors/destination-weaviate/main.py @@ -0,0 +1,11 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# + + +import sys + +from destination_weaviate import DestinationWeaviate + +if __name__ == "__main__": + DestinationWeaviate().run(sys.argv[1:]) diff --git a/airbyte-integrations/connectors/destination-weaviate/requirements.txt b/airbyte-integrations/connectors/destination-weaviate/requirements.txt new file mode 100644 index 000000000000..d6e1198b1ab1 --- /dev/null +++ b/airbyte-integrations/connectors/destination-weaviate/requirements.txt @@ -0,0 +1 @@ +-e . diff --git a/airbyte-integrations/connectors/destination-weaviate/setup.py b/airbyte-integrations/connectors/destination-weaviate/setup.py new file mode 100644 index 000000000000..ea1e8b7e3e9c --- /dev/null +++ b/airbyte-integrations/connectors/destination-weaviate/setup.py @@ -0,0 +1,29 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# + + +from setuptools import find_packages, setup + +MAIN_REQUIREMENTS = [ + "airbyte-cdk", + "weaviate-client==3.9.0" +] + +TEST_REQUIREMENTS = [ + "pytest~=6.2", + "docker" +] + +setup( + name="destination_weaviate", + description="Destination implementation for Weaviate.", + author="Airbyte", + author_email="contact@airbyte.io", + packages=find_packages(), + install_requires=MAIN_REQUIREMENTS, + package_data={"": ["*.json"]}, + extras_require={ + "tests": TEST_REQUIREMENTS, + }, +) diff --git a/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py b/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py new file mode 100644 index 000000000000..dddaea0060fa --- /dev/null +++ b/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py @@ -0,0 +1,7 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# + + +def test_example_method(): + assert True From 77431fa79689fae22bae6d8c79644b71feada688 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Mon, 5 Dec 2022 12:44:00 -0800 Subject: [PATCH 02/42] Fix formatting and standards --- .../destination_weaviate/client.py | 10 +++++++--- .../destination_weaviate/destination.py | 8 +++----- .../destination_weaviate/writer.py | 3 +++ .../integration_tests/integration_test.py | 8 ++++---- .../connectors/destination-weaviate/setup.py | 10 ++-------- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index c77f36704dd2..e900c30a2859 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -1,7 +1,11 @@ -import weaviate +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# -from typing import Any, Mapping import uuid +from typing import Any, Mapping + +import weaviate class Client: @@ -13,7 +17,7 @@ def __init__(self, config: Mapping[str, Any]): def queue_write_operation(self, stream_name: str, record: Mapping): # TODO need to handle case where original DB ID is not a UUID id = "" - if record.get('id'): + if record.get("id"): id = record.get("id") del record["id"] else: diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py index 457966074141..40b67b055f86 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py @@ -9,13 +9,12 @@ from airbyte_cdk.destinations import Destination from airbyte_cdk.models import AirbyteConnectionStatus, AirbyteMessage, ConfiguredAirbyteCatalog, DestinationSyncMode, Status, Type - from .client import Client class DestinationWeaviate(Destination): def write( - self, config: Mapping[str, Any], configured_catalog: ConfiguredAirbyteCatalog, input_messages: Iterable[AirbyteMessage] + self, config: Mapping[str, Any], configured_catalog: ConfiguredAirbyteCatalog, input_messages: Iterable[AirbyteMessage] ) -> Iterable[AirbyteMessage]: """ @@ -35,7 +34,7 @@ def write( """ client = Client(config) # TODO add support for overwrite mode - #for configured_stream in configured_catalog.streams: + # for configured_stream in configured_catalog.streams: # if configured_stream.destination_sync_mode == DestinationSyncMode.overwrite: # client.delete_stream_entries(configured_stream.stream.name) @@ -71,8 +70,7 @@ def check(self, logger: AirbyteLogger, config: Mapping[str, Any]) -> AirbyteConn client = Client.get_weaviate_client(config) ready = client.is_ready() if not ready: - return AirbyteConnectionStatus(status=Status.FAILED, - message=f"Weaviate server {config.get('url')} not ready") + return AirbyteConnectionStatus(status=Status.FAILED, message=f"Weaviate server {config.get('url')} not ready") return AirbyteConnectionStatus(status=Status.SUCCEEDED) except Exception as e: return AirbyteConnectionStatus(status=Status.FAILED, message=f"An exception occurred: {repr(e)}") diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/writer.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/writer.py index e69de29bb2d1..1100c1c58cf5 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/writer.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/writer.py @@ -0,0 +1,3 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index 6e3c523ec5ab..45b2f7ed13ce 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -3,10 +3,11 @@ # import json -from typing import Any, Dict, List, Mapping -import time import logging +import time +from typing import Any, Dict, List, Mapping +import docker import pytest from airbyte_cdk import AirbyteLogger from airbyte_cdk.models import ( @@ -21,7 +22,6 @@ SyncMode, Type, ) -import docker from destination_weaviate import DestinationWeaviate from destination_weaviate.client import Client @@ -70,7 +70,7 @@ def setup_teardown(config: Mapping): client = None while retries > 0: try: - client = Client(config) + Client(config) break except Exception as e: logging.info(f"error connecting to weaviate with client. Retrying in 1 second. Exception: {e}") diff --git a/airbyte-integrations/connectors/destination-weaviate/setup.py b/airbyte-integrations/connectors/destination-weaviate/setup.py index ea1e8b7e3e9c..02595794924a 100644 --- a/airbyte-integrations/connectors/destination-weaviate/setup.py +++ b/airbyte-integrations/connectors/destination-weaviate/setup.py @@ -5,15 +5,9 @@ from setuptools import find_packages, setup -MAIN_REQUIREMENTS = [ - "airbyte-cdk", - "weaviate-client==3.9.0" -] +MAIN_REQUIREMENTS = ["airbyte-cdk", "weaviate-client==3.9.0"] -TEST_REQUIREMENTS = [ - "pytest~=6.2", - "docker" -] +TEST_REQUIREMENTS = ["pytest~=6.2", "docker"] setup( name="destination_weaviate", From 7b159bc718cdaf60196131086a8b5204cd024559 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Mon, 5 Dec 2022 12:48:44 -0800 Subject: [PATCH 03/42] Fix flake issue --- .../destination-weaviate/destination_weaviate/destination.py | 2 +- .../destination-weaviate/destination_weaviate/writer.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py index 40b67b055f86..10f83e93d1f9 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py @@ -7,7 +7,7 @@ from airbyte_cdk import AirbyteLogger from airbyte_cdk.destinations import Destination -from airbyte_cdk.models import AirbyteConnectionStatus, AirbyteMessage, ConfiguredAirbyteCatalog, DestinationSyncMode, Status, Type +from airbyte_cdk.models import AirbyteConnectionStatus, AirbyteMessage, ConfiguredAirbyteCatalog, Status, Type from .client import Client diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/writer.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/writer.py index 1100c1c58cf5..afff7653e69c 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/writer.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/writer.py @@ -1,3 +1,5 @@ # # Copyright (c) 2022 Airbyte, Inc., all rights reserved. # + +# From 9e56857339e55b7344cae7ee3b855f8f5bf50e01 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Mon, 5 Dec 2022 12:51:09 -0800 Subject: [PATCH 04/42] Fix unused client variable --- .../destination-weaviate/integration_tests/integration_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index 45b2f7ed13ce..58b29327c99f 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -67,7 +67,6 @@ def setup_teardown(config: Mapping): ) retries = 3 - client = None while retries > 0: try: Client(config) From 228c7c57b0892ae5053412aef7c2e1fb7800064c Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Wed, 7 Dec 2022 13:41:04 -0800 Subject: [PATCH 05/42] Add support for int based ID fields --- .../destination_weaviate/client.py | 2 + .../integration_tests/example-config.json | 2 +- .../integration_tests/integration_test.py | 38 ++++++++++++++++++- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index e900c30a2859..1d8a01549d3d 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -19,6 +19,8 @@ def queue_write_operation(self, stream_name: str, record: Mapping): id = "" if record.get("id"): id = record.get("id") + if isinstance(id, int): + id = uuid.UUID(int=id) del record["id"] else: id = uuid.uuid4() diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/example-config.json b/airbyte-integrations/connectors/destination-weaviate/integration_tests/example-config.json index cc10c7772dc2..acf07c93140c 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/example-config.json +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/example-config.json @@ -1 +1 @@ -{ "url": "http://localhost:8080"} +{ "url": "http://localhost:8081"} diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index 58b29327c99f..04bbf8ed13fa 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -63,7 +63,7 @@ def setup_teardown(config: Mapping): docker_client.containers.run( "semitechnologies/weaviate:1.16.1", detach=True, environment=env_vars, name=name, - ports={8080: ('127.0.0.1', 8080)} + ports={8080: ('127.0.0.1', 8081)} ) retries = 3 @@ -105,6 +105,16 @@ def _record(stream: str, title: str, word_count: int) -> AirbyteMessage: ) +def _record_with_id(stream: str, title: str, word_count: int, id: int) -> AirbyteMessage: + return AirbyteMessage( + type=Type.RECORD, record=AirbyteRecordMessage(stream=stream, data={ + "title": title, + "wordCount": word_count, + "id": id + }, emitted_at=0) + ) + + def retrieve_all_records(client: Client) -> List[AirbyteRecordMessage]: """retrieves and formats all Articles as Airbyte messages""" all_records = client.client.data_object.get(class_name="Article") @@ -140,3 +150,29 @@ def test_write(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, cl expected_records = [_record(append_stream, str(i), i) for i in range(5)] records_in_destination = retrieve_all_records(client) assert expected_records == records_in_destination, "Records in destination should match records expected" + +def test_write_id(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, client: Client): + """ + This test verifies that records can have an ID that's an integer + """ + append_stream = configured_catalog.streams[0].stream.name + first_state_message = _state({"state": "1"}) + first_record_chunk = [_record_with_id(append_stream, str(i), i, i) for i in range(1, 6)] + + destination = DestinationWeaviate() + + expected_states = [first_state_message] + output_states = list( + destination.write( + config, configured_catalog, [*first_record_chunk, first_state_message] + ) + ) + assert expected_states == output_states, "Checkpoint state messages were expected from the destination" + + records_in_destination = retrieve_all_records(client) + assert len(records_in_destination) == 5, "Expecting there should be 5 records" + + expected_records = [_record(append_stream, str(i), i) for i in range(1, 6)] + for expected, actual in zip(expected_records, records_in_destination): + assert expected.record.data.get("title") == actual.record.data.get("title"), "Titles should match" + assert expected.record.data.get("wordCount") == actual.record.data.get("wordCount"), "Titles should match" From 875be1bf3bdebde8f29c1b7933b16cd727049e97 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Wed, 7 Dec 2022 16:44:22 -0800 Subject: [PATCH 06/42] Ensure stream name meets Weaviate class reqs --- .../destination-weaviate/destination_weaviate/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index 1d8a01549d3d..20799cf2e9b5 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -25,7 +25,7 @@ def queue_write_operation(self, stream_name: str, record: Mapping): else: id = uuid.uuid4() - self.client.batch.add_data_object(record, stream_name, id) + self.client.batch.add_data_object(record, stream_name.title(), id) if self.client.batch.num_objects() >= self.batch_size: self.client.batch.create_objects() From 8c9ad5b43c8ddff73ae8d0622d200d5ba02195e4 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Wed, 7 Dec 2022 21:50:49 -0800 Subject: [PATCH 07/42] add integration test for using pokemon as source --- .../destination_weaviate/client.py | 10 ++++- .../integration_tests/integration_test.py | 38 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index 20799cf2e9b5..72c7cdbaa3c4 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -3,6 +3,7 @@ # import uuid +import logging from typing import Any, Mapping import weaviate @@ -27,10 +28,15 @@ def queue_write_operation(self, stream_name: str, record: Mapping): self.client.batch.add_data_object(record, stream_name.title(), id) if self.client.batch.num_objects() >= self.batch_size: - self.client.batch.create_objects() + self.flush() def flush(self): - self.client.batch.create_objects() + # TODO add error handling + results = self.client.batch.create_objects() + for result in results: + errors = result.get("result", {}).get("errors", []) + if errors: + logging.error(f"Object {result.get('id')} had errors: {errors}") @staticmethod def get_weaviate_client(config: Mapping[str, Any]) -> weaviate.Client: diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index 04bbf8ed13fa..29faa688f965 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -22,6 +22,7 @@ SyncMode, Type, ) +import requests from destination_weaviate import DestinationWeaviate from destination_weaviate.client import Client @@ -44,6 +45,19 @@ def configured_catalog_fixture() -> ConfiguredAirbyteCatalog: return ConfiguredAirbyteCatalog(streams=[append_stream]) +@pytest.fixture(name="pokemon_catalog") +def pokemon_catalog_fixture() -> ConfiguredAirbyteCatalog: + stream_schema = requests.get("https://raw.githubusercontent.com/airbytehq/airbyte/master/airbyte-integrations" + "/connectors/source-pokeapi/source_pokeapi/schemas/pokemon.json").json() + + append_stream = ConfiguredAirbyteStream( + stream=AirbyteStream(name="Pokemon", json_schema=stream_schema, supported_sync_modes=[SyncMode.incremental]), + sync_mode=SyncMode.incremental, + destination_sync_mode=DestinationSyncMode.append, + ) + + return ConfiguredAirbyteCatalog(streams=[append_stream]) + @pytest.fixture(autouse=True) def setup_teardown(config: Mapping): @@ -105,6 +119,12 @@ def _record(stream: str, title: str, word_count: int) -> AirbyteMessage: ) +def _pokemon_record(pokemon: str): + url = f"https://pokeapi.co/api/v2/pokemon/{pokemon}" + data = requests.get(url).json() + return AirbyteMessage(type=Type.RECORD, record=AirbyteRecordMessage(stream="pokemon", data=data, emitted_at=0)) + + def _record_with_id(stream: str, title: str, word_count: int, id: int) -> AirbyteMessage: return AirbyteMessage( type=Type.RECORD, record=AirbyteRecordMessage(stream=stream, data={ @@ -151,6 +171,7 @@ def test_write(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, cl records_in_destination = retrieve_all_records(client) assert expected_records == records_in_destination, "Records in destination should match records expected" + def test_write_id(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, client: Client): """ This test verifies that records can have an ID that's an integer @@ -176,3 +197,20 @@ def test_write_id(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, for expected, actual in zip(expected_records, records_in_destination): assert expected.record.data.get("title") == actual.record.data.get("title"), "Titles should match" assert expected.record.data.get("wordCount") == actual.record.data.get("wordCount"), "Titles should match" + + +def test_write_pokemon_source_pikachu(config: Mapping, pokemon_catalog: ConfiguredAirbyteCatalog, client: Client): + destination = DestinationWeaviate() + + first_state_message = _state({"state": "1"}) + output_states = list( + destination.write( + config, pokemon_catalog, [_pokemon_record("pikachu"), first_state_message] + ) + ) + + expected_states = [first_state_message] + assert expected_states == output_states, "Checkpoint state messages were expected from the destination" + + records_in_destination = retrieve_all_records(client) + assert len(records_in_destination) == 1, "Expecting there should be 1 record" From 5be647fd4bff206bfa95ba9a35df5ee27c1c223e Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Thu, 8 Dec 2022 11:21:47 -0800 Subject: [PATCH 08/42] handle nested objects by converting to json string --- .../destination_weaviate/client.py | 13 ++++++++++++ .../integration_tests/integration_test.py | 21 +++++++++++++------ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index 72c7cdbaa3c4..7577923afad7 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -4,6 +4,7 @@ import uuid import logging +import json from typing import Any, Mapping import weaviate @@ -26,6 +27,18 @@ def queue_write_operation(self, stream_name: str, record: Mapping): else: id = uuid.uuid4() + # TODO support nested objects instead of converting to json string + for k, v in record.items(): + # TODO better support empty list by inferring from catalog + if isinstance(v, list) and len(v) == 0: + record[k] = json.dumps(v) + if isinstance(v, list) and len(v) > 0 and isinstance(v[0], dict): + record[k] = json.dumps(v) + if isinstance(v, dict): + record[k] = json.dumps(v) + + logging.info(record.get("past_types")) + self.client.batch.add_data_object(record, stream_name.title(), id) if self.client.batch.num_objects() >= self.batch_size: self.flush() diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index 29faa688f965..d1825e01007d 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -135,7 +135,7 @@ def _record_with_id(stream: str, title: str, word_count: int, id: int) -> Airbyt ) -def retrieve_all_records(client: Client) -> List[AirbyteRecordMessage]: +def retrieve_all_articles(client: Client) -> List[AirbyteRecordMessage]: """retrieves and formats all Articles as Airbyte messages""" all_records = client.client.data_object.get(class_name="Article") out = [] @@ -146,6 +146,11 @@ def retrieve_all_records(client: Client) -> List[AirbyteRecordMessage]: return out +def retrieve_all_pokemons(client: Client) -> List[dict]: + """retrieves and formats all Articles as Airbyte messages""" + return client.client.data_object.get(class_name="Pokemon") + + def test_write(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, client: Client): """ This test verifies that: @@ -168,7 +173,7 @@ def test_write(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, cl assert expected_states == output_states, "Checkpoint state messages were expected from the destination" expected_records = [_record(append_stream, str(i), i) for i in range(5)] - records_in_destination = retrieve_all_records(client) + records_in_destination = retrieve_all_articles(client) assert expected_records == records_in_destination, "Records in destination should match records expected" @@ -190,7 +195,7 @@ def test_write_id(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, ) assert expected_states == output_states, "Checkpoint state messages were expected from the destination" - records_in_destination = retrieve_all_records(client) + records_in_destination = retrieve_all_articles(client) assert len(records_in_destination) == 5, "Expecting there should be 5 records" expected_records = [_record(append_stream, str(i), i) for i in range(1, 6)] @@ -203,14 +208,18 @@ def test_write_pokemon_source_pikachu(config: Mapping, pokemon_catalog: Configur destination = DestinationWeaviate() first_state_message = _state({"state": "1"}) + pikachu = _pokemon_record("pikachu") output_states = list( destination.write( - config, pokemon_catalog, [_pokemon_record("pikachu"), first_state_message] + config, pokemon_catalog, [pikachu, first_state_message] ) ) expected_states = [first_state_message] assert expected_states == output_states, "Checkpoint state messages were expected from the destination" - records_in_destination = retrieve_all_records(client) - assert len(records_in_destination) == 1, "Expecting there should be 1 record" + records_in_destination = retrieve_all_pokemons(client) + assert len(records_in_destination["objects"]) == 1, "Expecting there should be 1 record" + + actual = records_in_destination["objects"][0] + assert actual["properties"]["name"] == pikachu.record.data.get("name"), "Names should match" From 1de0ca2d4d4ab149db081a1243f42244592dd9a0 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Thu, 8 Dec 2022 22:24:00 -0800 Subject: [PATCH 09/42] create schema for transforming data to weaviate --- .../destination_weaviate/client.py | 19 ++++++++----------- .../destination_weaviate/destination.py | 17 ++++++++++++++++- .../integration_tests/integration_test.py | 6 +++--- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index 7577923afad7..f2b5b4659a09 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -11,10 +11,11 @@ class Client: - def __init__(self, config: Mapping[str, Any]): + def __init__(self, config: Mapping[str, Any], schema: Mapping[str, str]): self.client = self.get_weaviate_client(config) self.config = config self.batch_size = 100 + self.schema = schema def queue_write_operation(self, stream_name: str, record: Mapping): # TODO need to handle case where original DB ID is not a UUID @@ -27,24 +28,20 @@ def queue_write_operation(self, stream_name: str, record: Mapping): else: id = uuid.uuid4() - # TODO support nested objects instead of converting to json string + # TODO support nested objects instead of converting to json string when weaviate supports this for k, v in record.items(): - # TODO better support empty list by inferring from catalog - if isinstance(v, list) and len(v) == 0: + if self.schema[stream_name].get(k, "") == "jsonify": record[k] = json.dumps(v) - if isinstance(v, list) and len(v) > 0 and isinstance(v[0], dict): - record[k] = json.dumps(v) - if isinstance(v, dict): - record[k] = json.dumps(v) - - logging.info(record.get("past_types")) + # Handling of empty list that's not part of defined schema otherwise Weaviate throws invalid string property + if isinstance(v, list) and len(v) == 0 and k not in self.schema[stream_name]: + record[k] = "" self.client.batch.add_data_object(record, stream_name.title(), id) if self.client.batch.num_objects() >= self.batch_size: self.flush() def flush(self): - # TODO add error handling + # TODO add error handling instead of just logging results = self.client.batch.create_objects() for result in results: errors = result.get("result", {}).get("errors", []) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py index 10f83e93d1f9..af82b3c23ddd 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py @@ -6,12 +6,27 @@ from typing import Any, Iterable, Mapping from airbyte_cdk import AirbyteLogger +import logging from airbyte_cdk.destinations import Destination from airbyte_cdk.models import AirbyteConnectionStatus, AirbyteMessage, ConfiguredAirbyteCatalog, Status, Type from .client import Client +def get_schema(configured_catalog: ConfiguredAirbyteCatalog) -> Mapping[str, Mapping[str, str]]: + schema = {} + for stream in configured_catalog.streams: + stream_schema = {} + for k, v in stream.stream.json_schema.get("properties").items(): + stream_schema[k] = "default" + if "array" in v.get("type", []) and "object" in v.get("items", {}).get("type", []): + stream_schema[k] = "jsonify" + if "object" in v.get("type", []): + stream_schema[k] = "jsonify" + schema[stream.stream.name] = stream_schema + return schema + + class DestinationWeaviate(Destination): def write( self, config: Mapping[str, Any], configured_catalog: ConfiguredAirbyteCatalog, input_messages: Iterable[AirbyteMessage] @@ -32,7 +47,7 @@ def write( :param input_messages: The stream of input messages received from the source :return: Iterable of AirbyteStateMessages wrapped in AirbyteMessage structs """ - client = Client(config) + client = Client(config, get_schema(configured_catalog)) # TODO add support for overwrite mode # for configured_stream in configured_catalog.streams: # if configured_stream.destination_sync_mode == DestinationSyncMode.overwrite: diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index d1825e01007d..3f416836a9d9 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -51,7 +51,7 @@ def pokemon_catalog_fixture() -> ConfiguredAirbyteCatalog: "/connectors/source-pokeapi/source_pokeapi/schemas/pokemon.json").json() append_stream = ConfiguredAirbyteStream( - stream=AirbyteStream(name="Pokemon", json_schema=stream_schema, supported_sync_modes=[SyncMode.incremental]), + stream=AirbyteStream(name="pokemon", json_schema=stream_schema, supported_sync_modes=[SyncMode.incremental]), sync_mode=SyncMode.incremental, destination_sync_mode=DestinationSyncMode.append, ) @@ -83,7 +83,7 @@ def setup_teardown(config: Mapping): retries = 3 while retries > 0: try: - Client(config) + Client(config, {}) break except Exception as e: logging.info(f"error connecting to weaviate with client. Retrying in 1 second. Exception: {e}") @@ -96,7 +96,7 @@ def setup_teardown(config: Mapping): @pytest.fixture(name="client") def client_fixture(config) -> Client: - return Client(config) + return Client(config, {}) def test_check_valid_config(config: Mapping): From 617ecf57e0a3d518ca30535284701bd5345dbc28 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Fri, 9 Dec 2022 10:41:52 -0800 Subject: [PATCH 10/42] Add docs for weaviate destination --- docs/integrations/destinations/weaviate.md | 62 ++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 docs/integrations/destinations/weaviate.md diff --git a/docs/integrations/destinations/weaviate.md b/docs/integrations/destinations/weaviate.md new file mode 100644 index 000000000000..ad95224794c7 --- /dev/null +++ b/docs/integrations/destinations/weaviate.md @@ -0,0 +1,62 @@ +# Weaviate + +## Features + +| Feature | Supported?\(Yes/No\) | Notes | +| :--- | :--- | :--- | +| Full Refresh Sync | No | | +| Incremental - Append Sync | Yes | | +| Incremental - Deduped History | No | | +| Namespaces | No | | +| Provide vector | No | | + +#### Output Schema + +Each stream will be output into its own class in Weaviate. The record fields will be stored as fields +in the Weaviate class. + +Dynamic Schema: Weaviate will automatically create a schema for the stream if no class was defined unless +you have disabled the Dynamic Schema feature in Weaviate. You can also create the class in Weaviate in advance +if you need more control over the schema in Weaviate. + +IDs: If your source table has an int based id stored as field name `id` then the +ID will automatically be converted to a UUID. Weaviate only supports ID to be a UUID. + + +## Getting Started + +Airbyte Cloud only supports connecting to your Weaviate Instance instance with TLS encryption and with a username and +password. + +## Getting Started \(Airbyte Open-Source\) + +#### Requirements + +To use the ClickHouse destination, you'll need: + +* A Weaviate cluster version 21.8.10.19 or above + +#### Configure Network Access + +Make sure your Weaviate database can be accessed by Airbyte. If your database is within a VPC, you may need to allow access from the IP you're using to expose Airbyte. + +#### **Permissions** + +You need a Weaviate user or use a Weaviate instance that's accessible to all + + +### Setup the ClickHouse Destination in Airbyte + +You should now have all the requirements needed to configure Weaviate as a destination in the UI. You'll need the following information to configure the Weaviate destination: + +* **URL** for example http://localhost:8080 or https://my-wcs.semi.network +* **Username** +* **Password** + + +## Changelog + +| Version | Date | Pull Request | Subject | +|:--------|:-----------| :--- |:---------------------------------------------| +| 0.1.0 | 2021-11-04 | [\#20094](https://github.com/airbytehq/airbyte/pull/20094) | Add ClickHouse destination | + From c8fd137c6e01423fd6529de3af67283e4427c156 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Fri, 9 Dec 2022 15:22:19 -0800 Subject: [PATCH 11/42] Remove pokemon-schema external dependency --- .../integration_tests/integration_test.py | 6 +- .../integration_tests/pokemon-schema.json | 271 ++++++++++++++++++ 2 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 airbyte-integrations/connectors/destination-weaviate/integration_tests/pokemon-schema.json diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index 3f416836a9d9..68f3216e51a0 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -6,6 +6,7 @@ import logging import time from typing import Any, Dict, List, Mapping +import os import docker import pytest @@ -47,8 +48,9 @@ def configured_catalog_fixture() -> ConfiguredAirbyteCatalog: @pytest.fixture(name="pokemon_catalog") def pokemon_catalog_fixture() -> ConfiguredAirbyteCatalog: - stream_schema = requests.get("https://raw.githubusercontent.com/airbytehq/airbyte/master/airbyte-integrations" - "/connectors/source-pokeapi/source_pokeapi/schemas/pokemon.json").json() + dirname = os.path.dirname(__file__) + file = open(os.path.join(dirname, "pokemon-schema.json")) + stream_schema = json.load(file) append_stream = ConfiguredAirbyteStream( stream=AirbyteStream(name="pokemon", json_schema=stream_schema, supported_sync_modes=[SyncMode.incremental]), diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/pokemon-schema.json b/airbyte-integrations/connectors/destination-weaviate/integration_tests/pokemon-schema.json new file mode 100644 index 000000000000..7c190d9cb6c4 --- /dev/null +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/pokemon-schema.json @@ -0,0 +1,271 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "name": { + "type": ["null", "string"] + }, + "base_experience": { + "type": ["null", "integer"] + }, + "height": { + "type": ["null", "integer"] + }, + "is_default ": { + "type": ["null", "boolean"] + }, + "order": { + "type": ["null", "integer"] + }, + "weight": { + "type": ["null", "integer"] + }, + "abilities": { + "type": ["null", "array"], + "items": { + "type": ["null", "object"], + "properties": { + "is_hidden": { + "type": ["null", "boolean"] + }, + "slot": { + "type": ["null", "integer"] + }, + "ability": { + "type": ["null", "object"], + "properties": { + "name": { + "type": ["null", "string"] + }, + "url": { + "type": ["null", "string"] + } + } + } + } + } + }, + "forms": { + "type": ["null", "array"], + "items": { + "type": ["null", "object"], + "properties": { + "name": { + "type": ["null", "string"] + }, + "url": { + "type": ["null", "string"] + } + } + } + }, + "game_indices": { + "type": ["null", "array"], + "items": { + "type": ["null", "object"], + "properties": { + "game_index": { + "type": ["null", "integer"] + }, + "version": { + "type": ["null", "object"], + "properties": { + "name": { + "type": ["null", "string"] + }, + "url": { + "type": ["null", "string"] + } + } + } + } + } + }, + "held_items": { + "type": ["null", "array"], + "items": { + "type": ["null", "object"], + "properties": { + "item": { + "type": ["null", "object"], + "properties": { + "name": { + "type": ["null", "string"] + }, + "url": { + "type": ["null", "string"] + } + } + }, + "version_details": { + "type": ["null", "array"], + "items": { + "type": ["null", "object"], + "properties": { + "version": { + "type": ["null", "object"], + "properties": { + "name": { + "type": ["null", "string"] + }, + "url": { + "type": ["null", "string"] + } + } + }, + "rarity": { + "type": ["null", "integer"] + } + } + } + } + } + } + }, + "location_area_encounters": { + "type": ["null", "string"] + }, + "moves": { + "type": ["null", "array"], + "items": { + "type": ["null", "object"], + "properties": { + "move": { + "type": ["null", "object"], + "properties": { + "name": { + "type": ["null", "string"] + }, + "url": { + "type": ["null", "string"] + } + } + }, + "version_group_details": { + "type": ["null", "array"], + "items": { + "type": ["null", "object"], + "properties": { + "move_learn_method": { + "type": ["null", "object"], + "properties": { + "name": { + "type": ["null", "string"] + }, + "url": { + "type": ["null", "string"] + } + } + }, + "version_group": { + "type": ["null", "object"], + "properties": { + "name": { + "type": ["null", "string"] + }, + "url": { + "type": ["null", "string"] + } + } + }, + "level_learned_at": { + "type": ["null", "integer"] + } + } + } + } + } + } + }, + "sprites": { + "type": ["null", "object"], + "properties": { + "front_default": { + "type": ["null", "string"] + }, + "front_shiny": { + "type": ["null", "string"] + }, + "front_female": { + "type": ["null", "string"] + }, + "front_shiny_female": { + "type": ["null", "string"] + }, + "back_default": { + "type": ["null", "string"] + }, + "back_shiny": { + "type": ["null", "string"] + }, + "back_female": { + "type": ["null", "string"] + }, + "back_shiny_female": { + "type": ["null", "string"] + } + } + }, + "species": { + "type": ["null", "object"], + "properties": { + "name": { + "type": ["null", "string"] + }, + "url": { + "type": ["null", "string"] + } + } + }, + "stats": { + "type": ["null", "array"], + "items": { + "type": ["null", "object"], + "properties": { + "stat": { + "type": ["null", "object"], + "properties": { + "name": { + "type": ["null", "string"] + }, + "url": { + "type": ["null", "string"] + } + } + }, + "effort": { + "type": ["null", "integer"] + }, + "base_stat": { + "type": ["null", "integer"] + } + } + } + }, + "types": { + "type": ["null", "array"], + "items": { + "type": ["null", "object"], + "properties": { + "slot": { + "type": ["null", "integer"] + }, + "type": { + "type": ["null", "object"], + "properties": { + "name": { + "type": ["null", "string"] + }, + "url": { + "type": ["null", "string"] + } + } + } + } + } + } + } +} From 1845194112ba68d95c38805e051b1e27384c4605 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Fri, 9 Dec 2022 15:28:13 -0800 Subject: [PATCH 12/42] Remove pikachu integration test external dep --- .../integration_tests/integration_test.py | 14 +++++++++----- .../integration_tests/pokemon-pikachu.json | 1 + 2 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 airbyte-integrations/connectors/destination-weaviate/integration_tests/pokemon-pikachu.json diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index 68f3216e51a0..49b1b9829f14 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -46,11 +46,16 @@ def configured_catalog_fixture() -> ConfiguredAirbyteCatalog: return ConfiguredAirbyteCatalog(streams=[append_stream]) + +def load_json_file(path: str) -> Mapping: + dirname = os.path.dirname(__file__) + file = open(os.path.join(dirname, path)) + return json.load(file) + + @pytest.fixture(name="pokemon_catalog") def pokemon_catalog_fixture() -> ConfiguredAirbyteCatalog: - dirname = os.path.dirname(__file__) - file = open(os.path.join(dirname, "pokemon-schema.json")) - stream_schema = json.load(file) + stream_schema = load_json_file("pokemon-schema.json") append_stream = ConfiguredAirbyteStream( stream=AirbyteStream(name="pokemon", json_schema=stream_schema, supported_sync_modes=[SyncMode.incremental]), @@ -122,8 +127,7 @@ def _record(stream: str, title: str, word_count: int) -> AirbyteMessage: def _pokemon_record(pokemon: str): - url = f"https://pokeapi.co/api/v2/pokemon/{pokemon}" - data = requests.get(url).json() + data = load_json_file("pokemon-pikachu.json") return AirbyteMessage(type=Type.RECORD, record=AirbyteRecordMessage(stream="pokemon", data=data, emitted_at=0)) diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/pokemon-pikachu.json b/airbyte-integrations/connectors/destination-weaviate/integration_tests/pokemon-pikachu.json new file mode 100644 index 000000000000..602d8eb47a49 --- /dev/null +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/pokemon-pikachu.json @@ -0,0 +1 @@ +{"abilities":[{"ability":{"name":"static","url":"https://pokeapi.co/api/v2/ability/9/"},"is_hidden":false,"slot":1},{"ability":{"name":"lightning-rod","url":"https://pokeapi.co/api/v2/ability/31/"},"is_hidden":true,"slot":3}],"base_experience":112,"forms":[{"name":"pikachu","url":"https://pokeapi.co/api/v2/pokemon-form/25/"}],"game_indices":[{"game_index":84,"version":{"name":"red","url":"https://pokeapi.co/api/v2/version/1/"}},{"game_index":84,"version":{"name":"blue","url":"https://pokeapi.co/api/v2/version/2/"}},{"game_index":84,"version":{"name":"yellow","url":"https://pokeapi.co/api/v2/version/3/"}},{"game_index":25,"version":{"name":"gold","url":"https://pokeapi.co/api/v2/version/4/"}},{"game_index":25,"version":{"name":"silver","url":"https://pokeapi.co/api/v2/version/5/"}},{"game_index":25,"version":{"name":"crystal","url":"https://pokeapi.co/api/v2/version/6/"}},{"game_index":25,"version":{"name":"ruby","url":"https://pokeapi.co/api/v2/version/7/"}},{"game_index":25,"version":{"name":"sapphire","url":"https://pokeapi.co/api/v2/version/8/"}},{"game_index":25,"version":{"name":"emerald","url":"https://pokeapi.co/api/v2/version/9/"}},{"game_index":25,"version":{"name":"firered","url":"https://pokeapi.co/api/v2/version/10/"}},{"game_index":25,"version":{"name":"leafgreen","url":"https://pokeapi.co/api/v2/version/11/"}},{"game_index":25,"version":{"name":"diamond","url":"https://pokeapi.co/api/v2/version/12/"}},{"game_index":25,"version":{"name":"pearl","url":"https://pokeapi.co/api/v2/version/13/"}},{"game_index":25,"version":{"name":"platinum","url":"https://pokeapi.co/api/v2/version/14/"}},{"game_index":25,"version":{"name":"heartgold","url":"https://pokeapi.co/api/v2/version/15/"}},{"game_index":25,"version":{"name":"soulsilver","url":"https://pokeapi.co/api/v2/version/16/"}},{"game_index":25,"version":{"name":"black","url":"https://pokeapi.co/api/v2/version/17/"}},{"game_index":25,"version":{"name":"white","url":"https://pokeapi.co/api/v2/version/18/"}},{"game_index":25,"version":{"name":"black-2","url":"https://pokeapi.co/api/v2/version/21/"}},{"game_index":25,"version":{"name":"white-2","url":"https://pokeapi.co/api/v2/version/22/"}}],"height":4,"held_items":[{"item":{"name":"oran-berry","url":"https://pokeapi.co/api/v2/item/132/"},"version_details":[{"rarity":50,"version":{"name":"ruby","url":"https://pokeapi.co/api/v2/version/7/"}},{"rarity":50,"version":{"name":"sapphire","url":"https://pokeapi.co/api/v2/version/8/"}},{"rarity":50,"version":{"name":"emerald","url":"https://pokeapi.co/api/v2/version/9/"}},{"rarity":50,"version":{"name":"diamond","url":"https://pokeapi.co/api/v2/version/12/"}},{"rarity":50,"version":{"name":"pearl","url":"https://pokeapi.co/api/v2/version/13/"}},{"rarity":50,"version":{"name":"platinum","url":"https://pokeapi.co/api/v2/version/14/"}},{"rarity":50,"version":{"name":"heartgold","url":"https://pokeapi.co/api/v2/version/15/"}},{"rarity":50,"version":{"name":"soulsilver","url":"https://pokeapi.co/api/v2/version/16/"}},{"rarity":50,"version":{"name":"black","url":"https://pokeapi.co/api/v2/version/17/"}},{"rarity":50,"version":{"name":"white","url":"https://pokeapi.co/api/v2/version/18/"}}]},{"item":{"name":"light-ball","url":"https://pokeapi.co/api/v2/item/213/"},"version_details":[{"rarity":5,"version":{"name":"ruby","url":"https://pokeapi.co/api/v2/version/7/"}},{"rarity":5,"version":{"name":"sapphire","url":"https://pokeapi.co/api/v2/version/8/"}},{"rarity":5,"version":{"name":"emerald","url":"https://pokeapi.co/api/v2/version/9/"}},{"rarity":5,"version":{"name":"diamond","url":"https://pokeapi.co/api/v2/version/12/"}},{"rarity":5,"version":{"name":"pearl","url":"https://pokeapi.co/api/v2/version/13/"}},{"rarity":5,"version":{"name":"platinum","url":"https://pokeapi.co/api/v2/version/14/"}},{"rarity":5,"version":{"name":"heartgold","url":"https://pokeapi.co/api/v2/version/15/"}},{"rarity":5,"version":{"name":"soulsilver","url":"https://pokeapi.co/api/v2/version/16/"}},{"rarity":1,"version":{"name":"black","url":"https://pokeapi.co/api/v2/version/17/"}},{"rarity":1,"version":{"name":"white","url":"https://pokeapi.co/api/v2/version/18/"}},{"rarity":5,"version":{"name":"black-2","url":"https://pokeapi.co/api/v2/version/21/"}},{"rarity":5,"version":{"name":"white-2","url":"https://pokeapi.co/api/v2/version/22/"}},{"rarity":5,"version":{"name":"x","url":"https://pokeapi.co/api/v2/version/23/"}},{"rarity":5,"version":{"name":"y","url":"https://pokeapi.co/api/v2/version/24/"}},{"rarity":5,"version":{"name":"omega-ruby","url":"https://pokeapi.co/api/v2/version/25/"}},{"rarity":5,"version":{"name":"alpha-sapphire","url":"https://pokeapi.co/api/v2/version/26/"}},{"rarity":5,"version":{"name":"sun","url":"https://pokeapi.co/api/v2/version/27/"}},{"rarity":5,"version":{"name":"moon","url":"https://pokeapi.co/api/v2/version/28/"}},{"rarity":5,"version":{"name":"ultra-sun","url":"https://pokeapi.co/api/v2/version/29/"}},{"rarity":5,"version":{"name":"ultra-moon","url":"https://pokeapi.co/api/v2/version/30/"}}]}],"id":25,"is_default":true,"location_area_encounters":"https://pokeapi.co/api/v2/pokemon/25/encounters","moves":[{"move":{"name":"mega-punch","url":"https://pokeapi.co/api/v2/move/5/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"pay-day","url":"https://pokeapi.co/api/v2/move/6/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"thunder-punch","url":"https://pokeapi.co/api/v2/move/9/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"slam","url":"https://pokeapi.co/api/v2/move/21/"},"version_group_details":[{"level_learned_at":20,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":20,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":20,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":20,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":20,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":20,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":21,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":21,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":21,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":26,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":20,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":20,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":26,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":26,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":37,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":37,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":37,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":24,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":28,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"double-kick","url":"https://pokeapi.co/api/v2/move/24/"},"version_group_details":[{"level_learned_at":9,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}}]},{"move":{"name":"mega-kick","url":"https://pokeapi.co/api/v2/move/25/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"headbutt","url":"https://pokeapi.co/api/v2/move/29/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}}]},{"move":{"name":"body-slam","url":"https://pokeapi.co/api/v2/move/34/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"take-down","url":"https://pokeapi.co/api/v2/move/36/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}}]},{"move":{"name":"double-edge","url":"https://pokeapi.co/api/v2/move/38/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}}]},{"move":{"name":"tail-whip","url":"https://pokeapi.co/api/v2/move/39/"},"version_group_details":[{"level_learned_at":6,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":6,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":6,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":6,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":6,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":6,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":5,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":5,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":5,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":5,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":6,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":6,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":5,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":3,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"growl","url":"https://pokeapi.co/api/v2/move/45/"},"version_group_details":[{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":5,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":5,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":5,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":5,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"surf","url":"https://pokeapi.co/api/v2/move/57/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"stadium-surfing-pikachu","url":"https://pokeapi.co/api/v2/move-learn-method/5/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"stadium-surfing-pikachu","url":"https://pokeapi.co/api/v2/move-learn-method/5/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"submission","url":"https://pokeapi.co/api/v2/move/66/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}}]},{"move":{"name":"counter","url":"https://pokeapi.co/api/v2/move/68/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}}]},{"move":{"name":"seismic-toss","url":"https://pokeapi.co/api/v2/move/69/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}}]},{"move":{"name":"strength","url":"https://pokeapi.co/api/v2/move/70/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}}]},{"move":{"name":"thunder-shock","url":"https://pokeapi.co/api/v2/move/84/"},"version_group_details":[{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"thunderbolt","url":"https://pokeapi.co/api/v2/move/85/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":26,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":26,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":26,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":26,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":26,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":26,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":26,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":26,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":26,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":29,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":26,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":26,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":29,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":29,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":42,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":42,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":42,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":21,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":36,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"thunder-wave","url":"https://pokeapi.co/api/v2/move/86/"},"version_group_details":[{"level_learned_at":9,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":8,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":8,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":8,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":8,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":8,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":8,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":10,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":10,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":10,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":10,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":8,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":8,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":10,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":13,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":18,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":18,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":18,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":15,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":4,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"thunder","url":"https://pokeapi.co/api/v2/move/87/"},"version_group_details":[{"level_learned_at":43,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":41,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":41,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":41,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":41,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":41,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":41,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":45,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":45,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":45,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":50,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":41,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":41,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":50,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":50,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":58,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":58,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":58,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":30,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":44,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"dig","url":"https://pokeapi.co/api/v2/move/91/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"toxic","url":"https://pokeapi.co/api/v2/move/92/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}}]},{"move":{"name":"agility","url":"https://pokeapi.co/api/v2/move/97/"},"version_group_details":[{"level_learned_at":33,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":33,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":33,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":33,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":33,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":33,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":33,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":34,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":34,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":34,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":37,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":33,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":33,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":37,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":37,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":45,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":45,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":45,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":27,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":24,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"quick-attack","url":"https://pokeapi.co/api/v2/move/98/"},"version_group_details":[{"level_learned_at":16,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":11,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":11,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":11,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":11,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":11,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":11,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":13,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":13,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":13,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":13,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":11,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":11,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":13,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":10,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":10,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":10,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":10,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":6,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"rage","url":"https://pokeapi.co/api/v2/move/99/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}}]},{"move":{"name":"mimic","url":"https://pokeapi.co/api/v2/move/102/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}}]},{"move":{"name":"double-team","url":"https://pokeapi.co/api/v2/move/104/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":15,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":15,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":15,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":15,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":15,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":15,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":18,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":18,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":18,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":21,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":15,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":15,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":21,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":21,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":23,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":23,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":23,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":12,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":8,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"defense-curl","url":"https://pokeapi.co/api/v2/move/111/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}}]},{"move":{"name":"light-screen","url":"https://pokeapi.co/api/v2/move/113/"},"version_group_details":[{"level_learned_at":50,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":50,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":50,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":50,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":50,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":50,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":42,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":42,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":42,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":45,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":50,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":50,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":45,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":45,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":53,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":53,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":53,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":18,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":40,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"reflect","url":"https://pokeapi.co/api/v2/move/115/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"bide","url":"https://pokeapi.co/api/v2/move/117/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}}]},{"move":{"name":"swift","url":"https://pokeapi.co/api/v2/move/129/"},"version_group_details":[{"level_learned_at":26,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"skull-bash","url":"https://pokeapi.co/api/v2/move/130/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}}]},{"move":{"name":"flash","url":"https://pokeapi.co/api/v2/move/148/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}}]},{"move":{"name":"rest","url":"https://pokeapi.co/api/v2/move/156/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"substitute","url":"https://pokeapi.co/api/v2/move/164/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"thief","url":"https://pokeapi.co/api/v2/move/168/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"snore","url":"https://pokeapi.co/api/v2/move/173/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"curse","url":"https://pokeapi.co/api/v2/move/174/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}}]},{"move":{"name":"reversal","url":"https://pokeapi.co/api/v2/move/179/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"protect","url":"https://pokeapi.co/api/v2/move/182/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"sweet-kiss","url":"https://pokeapi.co/api/v2/move/186/"},"version_group_details":[{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"mud-slap","url":"https://pokeapi.co/api/v2/move/189/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}}]},{"move":{"name":"zap-cannon","url":"https://pokeapi.co/api/v2/move/192/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}}]},{"move":{"name":"detect","url":"https://pokeapi.co/api/v2/move/197/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}}]},{"move":{"name":"endure","url":"https://pokeapi.co/api/v2/move/203/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"charm","url":"https://pokeapi.co/api/v2/move/204/"},"version_group_details":[{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"rollout","url":"https://pokeapi.co/api/v2/move/205/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}}]},{"move":{"name":"swagger","url":"https://pokeapi.co/api/v2/move/207/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}}]},{"move":{"name":"spark","url":"https://pokeapi.co/api/v2/move/209/"},"version_group_details":[{"level_learned_at":26,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":26,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":26,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":20,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"attract","url":"https://pokeapi.co/api/v2/move/213/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"sleep-talk","url":"https://pokeapi.co/api/v2/move/214/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"return","url":"https://pokeapi.co/api/v2/move/216/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}}]},{"move":{"name":"frustration","url":"https://pokeapi.co/api/v2/move/218/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}}]},{"move":{"name":"dynamic-punch","url":"https://pokeapi.co/api/v2/move/223/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}}]},{"move":{"name":"encore","url":"https://pokeapi.co/api/v2/move/227/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"iron-tail","url":"https://pokeapi.co/api/v2/move/231/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"hidden-power","url":"https://pokeapi.co/api/v2/move/237/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}}]},{"move":{"name":"rain-dance","url":"https://pokeapi.co/api/v2/move/240/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"rock-smash","url":"https://pokeapi.co/api/v2/move/249/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}}]},{"move":{"name":"uproar","url":"https://pokeapi.co/api/v2/move/253/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"facade","url":"https://pokeapi.co/api/v2/move/263/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"focus-punch","url":"https://pokeapi.co/api/v2/move/264/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}}]},{"move":{"name":"helping-hand","url":"https://pokeapi.co/api/v2/move/270/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"brick-break","url":"https://pokeapi.co/api/v2/move/280/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"knock-off","url":"https://pokeapi.co/api/v2/move/282/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}}]},{"move":{"name":"secret-power","url":"https://pokeapi.co/api/v2/move/290/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}}]},{"move":{"name":"signal-beam","url":"https://pokeapi.co/api/v2/move/324/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}}]},{"move":{"name":"covet","url":"https://pokeapi.co/api/v2/move/343/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}}]},{"move":{"name":"volt-tackle","url":"https://pokeapi.co/api/v2/move/344/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}}]},{"move":{"name":"calm-mind","url":"https://pokeapi.co/api/v2/move/347/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}}]},{"move":{"name":"shock-wave","url":"https://pokeapi.co/api/v2/move/351/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}}]},{"move":{"name":"natural-gift","url":"https://pokeapi.co/api/v2/move/363/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}}]},{"move":{"name":"feint","url":"https://pokeapi.co/api/v2/move/364/"},"version_group_details":[{"level_learned_at":29,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":29,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":29,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":34,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":34,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":34,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":21,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":21,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":21,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":16,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"fling","url":"https://pokeapi.co/api/v2/move/374/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"magnet-rise","url":"https://pokeapi.co/api/v2/move/393/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}}]},{"move":{"name":"nasty-plot","url":"https://pokeapi.co/api/v2/move/417/"},"version_group_details":[{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"discharge","url":"https://pokeapi.co/api/v2/move/435/"},"version_group_details":[{"level_learned_at":37,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":37,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":37,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":42,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":42,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":42,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":34,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":34,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":34,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":32,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"captivate","url":"https://pokeapi.co/api/v2/move/445/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}}]},{"move":{"name":"grass-knot","url":"https://pokeapi.co/api/v2/move/447/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"charge-beam","url":"https://pokeapi.co/api/v2/move/451/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}}]},{"move":{"name":"electro-ball","url":"https://pokeapi.co/api/v2/move/486/"},"version_group_details":[{"level_learned_at":18,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":18,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":18,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":13,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":13,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":13,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":12,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"round","url":"https://pokeapi.co/api/v2/move/496/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"echoed-voice","url":"https://pokeapi.co/api/v2/move/497/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}}]},{"move":{"name":"volt-switch","url":"https://pokeapi.co/api/v2/move/521/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"electroweb","url":"https://pokeapi.co/api/v2/move/527/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"wild-charge","url":"https://pokeapi.co/api/v2/move/528/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":50,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":50,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":50,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"draining-kiss","url":"https://pokeapi.co/api/v2/move/577/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"play-rough","url":"https://pokeapi.co/api/v2/move/583/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"play-nice","url":"https://pokeapi.co/api/v2/move/589/"},"version_group_details":[{"level_learned_at":7,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":7,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":7,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":7,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"confide","url":"https://pokeapi.co/api/v2/move/590/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}}]},{"move":{"name":"electric-terrain","url":"https://pokeapi.co/api/v2/move/604/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"machine","url":"https://pokeapi.co/api/v2/move-learn-method/4/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"nuzzle","url":"https://pokeapi.co/api/v2/move/609/"},"version_group_details":[{"level_learned_at":23,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":29,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":29,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":29,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]},{"move":{"name":"laser-focus","url":"https://pokeapi.co/api/v2/move/673/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}}]},{"move":{"name":"rising-voltage","url":"https://pokeapi.co/api/v2/move/804/"},"version_group_details":[{"level_learned_at":0,"move_learn_method":{"name":"tutor","url":"https://pokeapi.co/api/v2/move-learn-method/3/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}}]}],"name":"pikachu","order":35,"past_types":[],"species":{"name":"pikachu","url":"https://pokeapi.co/api/v2/pokemon-species/25/"},"sprites":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/25.png","back_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/female/25.png","back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/25.png","back_shiny_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/female/25.png","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/25.png","front_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/female/25.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/25.png","front_shiny_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/female/25.png","other":{"dream_world":{"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/25.svg","front_female":null},"home":{"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/home/25.png","front_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/home/female/25.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/home/shiny/25.png","front_shiny_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/home/shiny/female/25.png"},"official-artwork":{"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/25.png"}},"versions":{"generation-i":{"red-blue":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/back/25.png","back_gray":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/back/gray/25.png","back_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/transparent/back/25.png","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/25.png","front_gray":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/gray/25.png","front_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/transparent/25.png"},"yellow":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/yellow/back/25.png","back_gray":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/yellow/back/gray/25.png","back_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/yellow/transparent/back/25.png","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/yellow/25.png","front_gray":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/yellow/gray/25.png","front_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/yellow/transparent/25.png"}},"generation-ii":{"crystal":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/back/25.png","back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/back/shiny/25.png","back_shiny_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/transparent/back/shiny/25.png","back_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/transparent/back/25.png","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/25.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/shiny/25.png","front_shiny_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/transparent/shiny/25.png","front_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/transparent/25.png"},"gold":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/gold/back/25.png","back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/gold/back/shiny/25.png","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/gold/25.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/gold/shiny/25.png","front_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/gold/transparent/25.png"},"silver":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/silver/back/25.png","back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/silver/back/shiny/25.png","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/silver/25.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/silver/shiny/25.png","front_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/silver/transparent/25.png"}},"generation-iii":{"emerald":{"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/emerald/25.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/emerald/shiny/25.png"},"firered-leafgreen":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/firered-leafgreen/back/25.png","back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/firered-leafgreen/back/shiny/25.png","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/firered-leafgreen/25.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/firered-leafgreen/shiny/25.png"},"ruby-sapphire":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/ruby-sapphire/back/25.png","back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/ruby-sapphire/back/shiny/25.png","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/ruby-sapphire/25.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/ruby-sapphire/shiny/25.png"}},"generation-iv":{"diamond-pearl":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/diamond-pearl/back/25.png","back_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/diamond-pearl/back/female/25.png","back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/diamond-pearl/back/shiny/25.png","back_shiny_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/diamond-pearl/back/shiny/female/25.png","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/diamond-pearl/25.png","front_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/diamond-pearl/female/25.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/diamond-pearl/shiny/25.png","front_shiny_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/diamond-pearl/shiny/female/25.png"},"heartgold-soulsilver":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/heartgold-soulsilver/back/25.png","back_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/heartgold-soulsilver/back/female/25.png","back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/heartgold-soulsilver/back/shiny/25.png","back_shiny_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/heartgold-soulsilver/back/shiny/female/25.png","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/heartgold-soulsilver/25.png","front_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/heartgold-soulsilver/female/25.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/heartgold-soulsilver/shiny/25.png","front_shiny_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/heartgold-soulsilver/shiny/female/25.png"},"platinum":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/platinum/back/25.png","back_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/platinum/back/female/25.png","back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/platinum/back/shiny/25.png","back_shiny_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/platinum/back/shiny/female/25.png","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/platinum/25.png","front_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/platinum/female/25.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/platinum/shiny/25.png","front_shiny_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/platinum/shiny/female/25.png"}},"generation-v":{"black-white":{"animated":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/animated/back/25.gif","back_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/animated/back/female/25.gif","back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/animated/back/shiny/25.gif","back_shiny_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/animated/back/shiny/female/25.gif","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/animated/25.gif","front_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/animated/female/25.gif","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/animated/shiny/25.gif","front_shiny_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/animated/shiny/female/25.gif"},"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/back/25.png","back_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/back/female/25.png","back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/back/shiny/25.png","back_shiny_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/back/shiny/female/25.png","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/25.png","front_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/female/25.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/shiny/25.png","front_shiny_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/shiny/female/25.png"}},"generation-vi":{"omegaruby-alphasapphire":{"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vi/omegaruby-alphasapphire/25.png","front_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vi/omegaruby-alphasapphire/female/25.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vi/omegaruby-alphasapphire/shiny/25.png","front_shiny_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vi/omegaruby-alphasapphire/shiny/female/25.png"},"x-y":{"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vi/x-y/25.png","front_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vi/x-y/female/25.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vi/x-y/shiny/25.png","front_shiny_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vi/x-y/shiny/female/25.png"}},"generation-vii":{"icons":{"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vii/icons/25.png","front_female":null},"ultra-sun-ultra-moon":{"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vii/ultra-sun-ultra-moon/25.png","front_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vii/ultra-sun-ultra-moon/female/25.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vii/ultra-sun-ultra-moon/shiny/25.png","front_shiny_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vii/ultra-sun-ultra-moon/shiny/female/25.png"}},"generation-viii":{"icons":{"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-viii/icons/25.png","front_female":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-viii/icons/female/25.png"}}}},"stats":[{"base_stat":35,"effort":0,"stat":{"name":"hp","url":"https://pokeapi.co/api/v2/stat/1/"}},{"base_stat":55,"effort":0,"stat":{"name":"attack","url":"https://pokeapi.co/api/v2/stat/2/"}},{"base_stat":40,"effort":0,"stat":{"name":"defense","url":"https://pokeapi.co/api/v2/stat/3/"}},{"base_stat":50,"effort":0,"stat":{"name":"special-attack","url":"https://pokeapi.co/api/v2/stat/4/"}},{"base_stat":50,"effort":0,"stat":{"name":"special-defense","url":"https://pokeapi.co/api/v2/stat/5/"}},{"base_stat":90,"effort":2,"stat":{"name":"speed","url":"https://pokeapi.co/api/v2/stat/6/"}}],"types":[{"slot":1,"type":{"name":"electric","url":"https://pokeapi.co/api/v2/type/13/"}}],"weight":60} \ No newline at end of file From 169025b75fcb7e3a045f3e91f60d658d0a7593a1 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Tue, 13 Dec 2022 12:49:24 -0800 Subject: [PATCH 13/42] Add large batch test case --- .../integration_tests/integration_test.py | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index 49b1b9829f14..a29a10d02ac8 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -126,7 +126,7 @@ def _record(stream: str, title: str, word_count: int) -> AirbyteMessage: ) -def _pokemon_record(pokemon: str): +def _pikachu_record(): data = load_json_file("pokemon-pikachu.json") return AirbyteMessage(type=Type.RECORD, record=AirbyteRecordMessage(stream="pokemon", data=data, emitted_at=0)) @@ -143,7 +143,7 @@ def _record_with_id(stream: str, title: str, word_count: int, id: int) -> Airbyt def retrieve_all_articles(client: Client) -> List[AirbyteRecordMessage]: """retrieves and formats all Articles as Airbyte messages""" - all_records = client.client.data_object.get(class_name="Article") + all_records = client.client.data_object.get(class_name="Article", ) out = [] for record in all_records.get("objects"): props = record["properties"] @@ -152,6 +152,13 @@ def retrieve_all_articles(client: Client) -> List[AirbyteRecordMessage]: return out +def count_articles(client: Client) -> int: + result = client.query.aggregate("Article") \ + .with_fields('meta { count }') \ + .do() + return result["data"]["Aggregate"]["Article"][0]["meta"]["count"] + + def retrieve_all_pokemons(client: Client) -> List[dict]: """retrieves and formats all Articles as Airbyte messages""" return client.client.data_object.get(class_name="Pokemon") @@ -183,6 +190,23 @@ def test_write(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, cl assert expected_records == records_in_destination, "Records in destination should match records expected" +def test_write_large_batch(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, client: Client): + append_stream = configured_catalog.streams[0].stream.name + first_state_message = _state({"state": "1"}) + first_record_chunk = [_record(append_stream, str(i), i) for i in range(400)] + + destination = DestinationWeaviate() + + expected_states = [first_state_message] + output_states = list( + destination.write( + config, configured_catalog, [*first_record_chunk, first_state_message] + ) + ) + assert expected_states == output_states, "Checkpoint state messages were expected from the destination" + assert count_articles(client.client) == 400, "There should be 400 records in weaviate" + + def test_write_id(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, client: Client): """ This test verifies that records can have an ID that's an integer @@ -214,7 +238,7 @@ def test_write_pokemon_source_pikachu(config: Mapping, pokemon_catalog: Configur destination = DestinationWeaviate() first_state_message = _state({"state": "1"}) - pikachu = _pokemon_record("pikachu") + pikachu = _pikachu_record() output_states = list( destination.write( config, pokemon_catalog, [pikachu, first_state_message] From 2f440ae611eb94e5bfb32350d947f4479b1e1d06 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Tue, 13 Dec 2022 13:04:42 -0800 Subject: [PATCH 14/42] add test for second sync --- .../integration_tests/integration_test.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index a29a10d02ac8..ec9805c8ef15 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -207,6 +207,24 @@ def test_write_large_batch(config: Mapping, configured_catalog: ConfiguredAirbyt assert count_articles(client.client) == 400, "There should be 400 records in weaviate" +def test_write_second_sync(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, client: Client): + append_stream = configured_catalog.streams[0].stream.name + first_state_message = _state({"state": "1"}) + second_state_message = _state({"state": "2"}) + first_record_chunk = [_record(append_stream, str(i), i) for i in range(5)] + + destination = DestinationWeaviate() + + expected_states = [first_state_message, second_state_message] + output_states = list( + destination.write( + config, configured_catalog, [*first_record_chunk, first_state_message, *first_record_chunk, second_state_message] + ) + ) + assert expected_states == output_states, "Checkpoint state messages were expected from the destination" + assert count_articles(client.client) == 10, "First and second state should have flushed a total of 10 articles" + + def test_write_id(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, client: Client): """ This test verifies that records can have an ID that's an integer From 3a898bf1d71837bbc737c151c3b898a379f4bf40 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Tue, 13 Dec 2022 20:35:12 -0800 Subject: [PATCH 15/42] Fix issue with fields starting with uppercase --- .../destination_weaviate/client.py | 3 + .../exchange_rate_catalog.json | 23 +++++ .../integration_tests/integration_test.py | 87 +++++++++++++------ 3 files changed, 87 insertions(+), 26 deletions(-) create mode 100644 airbyte-integrations/connectors/destination-weaviate/integration_tests/exchange_rate_catalog.json diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index f2b5b4659a09..326e89087b00 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -36,6 +36,9 @@ def queue_write_operation(self, stream_name: str, record: Mapping): if isinstance(v, list) and len(v) == 0 and k not in self.schema[stream_name]: record[k] = "" + # Property names in Weaviate have to start with lowercase letter + record = {k[0].lower() + k[1:]: v for k, v in record.items()} + self.client.batch.add_data_object(record, stream_name.title(), id) if self.client.batch.num_objects() >= self.batch_size: self.flush() diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/exchange_rate_catalog.json b/airbyte-integrations/connectors/destination-weaviate/integration_tests/exchange_rate_catalog.json new file mode 100644 index 000000000000..f8cf0bd05ffd --- /dev/null +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/exchange_rate_catalog.json @@ -0,0 +1,23 @@ +{ + "type" : "object", + "properties" : { + "id" : { + "type" : "integer" + }, + "currency" : { + "type" : "string" + }, + "date" : { + "type" : "string" + }, + "HKD" : { + "type" : "number" + }, + "NZD" : { + "type" : "number" + }, + "USD" : { + "type" : "number" + } + } +} \ No newline at end of file diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index ec9805c8ef15..2de9ebd0b876 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -34,8 +34,17 @@ def config_fixture() -> Mapping[str, Any]: return json.loads(f.read()) -@pytest.fixture(name="configured_catalog") -def configured_catalog_fixture() -> ConfiguredAirbyteCatalog: +def create_catalog(stream_name: str, stream_schema: Mapping[str, Any]) -> ConfiguredAirbyteCatalog: + append_stream = ConfiguredAirbyteStream( + stream=AirbyteStream(name=stream_name, json_schema=stream_schema, supported_sync_modes=[SyncMode.incremental]), + sync_mode=SyncMode.incremental, + destination_sync_mode=DestinationSyncMode.append, + ) + return ConfiguredAirbyteCatalog(streams=[append_stream]) + + +@pytest.fixture(name="article_catalog") +def article_catalog_fixture() -> ConfiguredAirbyteCatalog: stream_schema = {"type": "object", "properties": {"title": {"type": "str"}, "wordCount": {"type": "integer"}}} append_stream = ConfiguredAirbyteStream( @@ -120,7 +129,13 @@ def _state(data: Dict[str, Any]) -> AirbyteMessage: return AirbyteMessage(type=Type.STATE, state=AirbyteStateMessage(data=data)) -def _record(stream: str, title: str, word_count: int) -> AirbyteMessage: +def _record(stream: str, data: Mapping[str, Any]) -> AirbyteMessage: + return AirbyteMessage( + type=Type.RECORD, record=AirbyteRecordMessage(stream=stream, data=data, emitted_at=0) + ) + + +def _article_record(stream: str, title: str, word_count: int) -> AirbyteMessage: return AirbyteMessage( type=Type.RECORD, record=AirbyteRecordMessage(stream=stream, data={"title": title, "wordCount": word_count}, emitted_at=0) ) @@ -147,16 +162,16 @@ def retrieve_all_articles(client: Client) -> List[AirbyteRecordMessage]: out = [] for record in all_records.get("objects"): props = record["properties"] - out.append(_record("Article", props["title"], props["wordCount"])) + out.append(_article_record("Article", props["title"], props["wordCount"])) out.sort(key=lambda x: x.record.data.get("title")) return out -def count_articles(client: Client) -> int: - result = client.query.aggregate("Article") \ +def count_objects(client: Client, class_name: str) -> int: + result = client.query.aggregate(class_name) \ .with_fields('meta { count }') \ .do() - return result["data"]["Aggregate"]["Article"][0]["meta"]["count"] + return result["data"]["Aggregate"][class_name][0]["meta"]["count"] def retrieve_all_pokemons(client: Client) -> List[dict]: @@ -164,72 +179,92 @@ def retrieve_all_pokemons(client: Client) -> List[dict]: return client.client.data_object.get(class_name="Pokemon") -def test_write(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, client: Client): +def test_write(config: Mapping, article_catalog: ConfiguredAirbyteCatalog, client: Client): """ This test verifies that: TODO: 1. writing a stream in "overwrite" mode overwrites any existing data for that stream 2. writing a stream in "append" mode appends new records without deleting the old ones 3. The correct state message is output by the connector at the end of the sync """ - append_stream = configured_catalog.streams[0].stream.name + append_stream = article_catalog.streams[0].stream.name first_state_message = _state({"state": "1"}) - first_record_chunk = [_record(append_stream, str(i), i) for i in range(5)] + first_record_chunk = [_article_record(append_stream, str(i), i) for i in range(5)] destination = DestinationWeaviate() expected_states = [first_state_message] output_states = list( destination.write( - config, configured_catalog, [*first_record_chunk, first_state_message] + config, article_catalog, [*first_record_chunk, first_state_message] ) ) assert expected_states == output_states, "Checkpoint state messages were expected from the destination" - expected_records = [_record(append_stream, str(i), i) for i in range(5)] + expected_records = [_article_record(append_stream, str(i), i) for i in range(5)] records_in_destination = retrieve_all_articles(client) assert expected_records == records_in_destination, "Records in destination should match records expected" -def test_write_large_batch(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, client: Client): - append_stream = configured_catalog.streams[0].stream.name +def test_write_large_batch(config: Mapping, article_catalog: ConfiguredAirbyteCatalog, client: Client): + append_stream = article_catalog.streams[0].stream.name first_state_message = _state({"state": "1"}) - first_record_chunk = [_record(append_stream, str(i), i) for i in range(400)] + first_record_chunk = [_article_record(append_stream, str(i), i) for i in range(400)] destination = DestinationWeaviate() expected_states = [first_state_message] output_states = list( destination.write( - config, configured_catalog, [*first_record_chunk, first_state_message] + config, article_catalog, [*first_record_chunk, first_state_message] ) ) assert expected_states == output_states, "Checkpoint state messages were expected from the destination" - assert count_articles(client.client) == 400, "There should be 400 records in weaviate" + assert count_objects(client.client, "Article") == 400, "There should be 400 records in weaviate" -def test_write_second_sync(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, client: Client): - append_stream = configured_catalog.streams[0].stream.name +def test_write_second_sync(config: Mapping, article_catalog: ConfiguredAirbyteCatalog, client: Client): + append_stream = article_catalog.streams[0].stream.name first_state_message = _state({"state": "1"}) second_state_message = _state({"state": "2"}) - first_record_chunk = [_record(append_stream, str(i), i) for i in range(5)] + first_record_chunk = [_article_record(append_stream, str(i), i) for i in range(5)] destination = DestinationWeaviate() expected_states = [first_state_message, second_state_message] output_states = list( destination.write( - config, configured_catalog, [*first_record_chunk, first_state_message, *first_record_chunk, second_state_message] + config, article_catalog, [*first_record_chunk, first_state_message, *first_record_chunk, second_state_message] + ) + ) + assert expected_states == output_states, "Checkpoint state messages were expected from the destination" + assert count_objects(client.client, "Article") == 10, "First and second state should have flushed a total of 10 articles" + + +def test_line_break_characters(config: Mapping, client: Client): + stream_name = "currency" + stream_schema = load_json_file("exchange_rate_catalog.json") + catalog = create_catalog(stream_name, stream_schema) + first_state_message = _state({"state": "1"}) + data = {"id": 1, "currency": "USD\u2028", "date": "2020-03-\n31T00:00:00Z\r", "HKD": 10.1, "NZD": 700.1} + first_record_chunk = [_record(stream_name, data)] + + destination = DestinationWeaviate() + + expected_states = [first_state_message] + output_states = list( + destination.write( + config, catalog, [*first_record_chunk, first_state_message] ) ) assert expected_states == output_states, "Checkpoint state messages were expected from the destination" - assert count_articles(client.client) == 10, "First and second state should have flushed a total of 10 articles" + assert count_objects(client.client, "Currency") == 1, "First and second state should have flushed a total of 10 articles" -def test_write_id(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, client: Client): +def test_write_id(config: Mapping, article_catalog: ConfiguredAirbyteCatalog, client: Client): """ This test verifies that records can have an ID that's an integer """ - append_stream = configured_catalog.streams[0].stream.name + append_stream = article_catalog.streams[0].stream.name first_state_message = _state({"state": "1"}) first_record_chunk = [_record_with_id(append_stream, str(i), i, i) for i in range(1, 6)] @@ -238,7 +273,7 @@ def test_write_id(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, expected_states = [first_state_message] output_states = list( destination.write( - config, configured_catalog, [*first_record_chunk, first_state_message] + config, article_catalog, [*first_record_chunk, first_state_message] ) ) assert expected_states == output_states, "Checkpoint state messages were expected from the destination" @@ -246,7 +281,7 @@ def test_write_id(config: Mapping, configured_catalog: ConfiguredAirbyteCatalog, records_in_destination = retrieve_all_articles(client) assert len(records_in_destination) == 5, "Expecting there should be 5 records" - expected_records = [_record(append_stream, str(i), i) for i in range(1, 6)] + expected_records = [_article_record(append_stream, str(i), i) for i in range(1, 6)] for expected, actual in zip(expected_records, records_in_destination): assert expected.record.data.get("title") == actual.record.data.get("title"), "Titles should match" assert expected.record.data.get("wordCount") == actual.record.data.get("wordCount"), "Titles should match" From c246173de05cfee111f2d780d902539e4f064102 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Tue, 13 Dec 2022 20:51:41 -0800 Subject: [PATCH 16/42] add more checks to line_break test --- .../integration_tests/integration_test.py | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index 2de9ebd0b876..a422c05b2553 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -23,7 +23,6 @@ SyncMode, Type, ) -import requests from destination_weaviate import DestinationWeaviate from destination_weaviate.client import Client @@ -95,6 +94,7 @@ def setup_teardown(config: Mapping): "semitechnologies/weaviate:1.16.1", detach=True, environment=env_vars, name=name, ports={8080: ('127.0.0.1', 8081)} ) + time.sleep(1) retries = 3 while retries > 0: @@ -167,18 +167,19 @@ def retrieve_all_articles(client: Client) -> List[AirbyteRecordMessage]: return out +def get_objects(client: Client, class_name: str) -> List[Mapping[str, Any]]: + """retrieves and formats all Articles as Airbyte messages""" + all_records = client.client.data_object.get(class_name=class_name) + return all_records.get("objects") + + def count_objects(client: Client, class_name: str) -> int: - result = client.query.aggregate(class_name) \ + result = client.client.query.aggregate(class_name) \ .with_fields('meta { count }') \ .do() return result["data"]["Aggregate"][class_name][0]["meta"]["count"] -def retrieve_all_pokemons(client: Client) -> List[dict]: - """retrieves and formats all Articles as Airbyte messages""" - return client.client.data_object.get(class_name="Pokemon") - - def test_write(config: Mapping, article_catalog: ConfiguredAirbyteCatalog, client: Client): """ This test verifies that: @@ -219,7 +220,7 @@ def test_write_large_batch(config: Mapping, article_catalog: ConfiguredAirbyteCa ) ) assert expected_states == output_states, "Checkpoint state messages were expected from the destination" - assert count_objects(client.client, "Article") == 400, "There should be 400 records in weaviate" + assert count_objects(client, "Article") == 400, "There should be 400 records in weaviate" def test_write_second_sync(config: Mapping, article_catalog: ConfiguredAirbyteCatalog, client: Client): @@ -237,7 +238,7 @@ def test_write_second_sync(config: Mapping, article_catalog: ConfiguredAirbyteCa ) ) assert expected_states == output_states, "Checkpoint state messages were expected from the destination" - assert count_objects(client.client, "Article") == 10, "First and second state should have flushed a total of 10 articles" + assert count_objects(client, "Article") == 10, "First and second state should have flushed a total of 10 articles" def test_line_break_characters(config: Mapping, client: Client): @@ -257,7 +258,12 @@ def test_line_break_characters(config: Mapping, client: Client): ) ) assert expected_states == output_states, "Checkpoint state messages were expected from the destination" - assert count_objects(client.client, "Currency") == 1, "First and second state should have flushed a total of 10 articles" + assert count_objects(client, "Currency") == 1, "There should be only 1 object of class currency in Weaviate" + actual = get_objects(client, "Currency")[0] + assert actual["properties"].get("date") == data.get("date"), "Dates with new line should match" + assert actual["properties"].get("hKD") == data.get("HKD"), "HKD should match hKD in Weaviate" + assert actual["properties"].get("nZD") == data.get("NZD") + assert actual["properties"].get("currency") == data.get("currency") def test_write_id(config: Mapping, article_catalog: ConfiguredAirbyteCatalog, client: Client): @@ -301,8 +307,8 @@ def test_write_pokemon_source_pikachu(config: Mapping, pokemon_catalog: Configur expected_states = [first_state_message] assert expected_states == output_states, "Checkpoint state messages were expected from the destination" - records_in_destination = retrieve_all_pokemons(client) - assert len(records_in_destination["objects"]) == 1, "Expecting there should be 1 record" + records_in_destination = get_objects(client, "Pokemon") + assert len(records_in_destination) == 1, "Expecting there should be 1 record" - actual = records_in_destination["objects"][0] + actual = records_in_destination[0] assert actual["properties"]["name"] == pikachu.record.data.get("name"), "Names should match" From d1f76d2df170e56dd74cd99e7bd2b07361a1b71d Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Wed, 14 Dec 2022 07:43:11 -0800 Subject: [PATCH 17/42] Update README for Weaviate --- docs/integrations/destinations/weaviate.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/integrations/destinations/weaviate.md b/docs/integrations/destinations/weaviate.md index ad95224794c7..5cb462124b1b 100644 --- a/docs/integrations/destinations/weaviate.md +++ b/docs/integrations/destinations/weaviate.md @@ -22,6 +22,9 @@ if you need more control over the schema in Weaviate. IDs: If your source table has an int based id stored as field name `id` then the ID will automatically be converted to a UUID. Weaviate only supports ID to be a UUID. +Any field name starting with an upper case letter will be converted to lower case. For example, +if you have a field name `USD` then that field will become `uSD`. This is due to a limitation +in Weaviate, see [this issue in Weaviate](https://github.com/semi-technologies/weaviate/issues/2438). ## Getting Started From 57e4432336455d530a0ed5dbb860d8a96885c43e Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Wed, 14 Dec 2022 07:49:31 -0800 Subject: [PATCH 18/42] Make batch_size configurable with 100 as default --- .../destination-weaviate/destination_weaviate/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index 326e89087b00..890e746ccc40 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -14,7 +14,7 @@ class Client: def __init__(self, config: Mapping[str, Any], schema: Mapping[str, str]): self.client = self.get_weaviate_client(config) self.config = config - self.batch_size = 100 + self.batch_size = int(config.get("batch_size", 100)) self.schema = schema def queue_write_operation(self, stream_name: str, record: Mapping): From 44d5ee014139005efc28982db5f2068232cc9e2f Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Wed, 14 Dec 2022 14:14:57 -0800 Subject: [PATCH 19/42] Add support for providing vectors --- .../destination_weaviate/client.py | 24 ++++++++++++-- .../destination_weaviate/spec.json | 12 +++++++ .../integration_tests/integration_test.py | 32 +++++++++++++++++-- .../unit_tests/unit_test.py | 13 ++++++-- 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index 890e746ccc40..19402d05fef1 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -5,17 +5,31 @@ import uuid import logging import json -from typing import Any, Mapping +from typing import Any, Mapping, List import weaviate +def parse_vectors(vectors_config: str) -> Mapping[str, str]: + vectors = {} + if not vectors_config: + return vectors + + vectors_list = vectors_config.replace(" ", "").split(",") + for vector in vectors_list: + stream_name, vector_column_name = vector.split(".") + vectors[stream_name] = vector_column_name + return vectors + + class Client: def __init__(self, config: Mapping[str, Any], schema: Mapping[str, str]): self.client = self.get_weaviate_client(config) self.config = config self.batch_size = int(config.get("batch_size", 100)) self.schema = schema + self.vectors = parse_vectors(config.get("vectors")) + def queue_write_operation(self, stream_name: str, record: Mapping): # TODO need to handle case where original DB ID is not a UUID @@ -38,8 +52,12 @@ def queue_write_operation(self, stream_name: str, record: Mapping): # Property names in Weaviate have to start with lowercase letter record = {k[0].lower() + k[1:]: v for k, v in record.items()} - - self.client.batch.add_data_object(record, stream_name.title(), id) + vector = None + if stream_name in self.vectors: + vector_column_name = self.vectors.get(stream_name) + vector = record.get(vector_column_name) + del record[vector_column_name] + self.client.batch.add_data_object(record, stream_name.title(), id, vector=vector) if self.client.batch.num_objects() >= self.batch_size: self.flush() diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json index bb5af766f33a..da8c9c6201b8 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json @@ -33,6 +33,18 @@ "password" : { "type" : "string", "description" : "Password used with OIDC authentication" + }, + "batch_size" : { + "type" : "integer", + "description" : "Batch size for writing to Weaviate", + "default" : 100 + }, + "vectors" : { + "type" : "string", + "description" : "Comma separated list of strings to configure which field holds the vector foreach stream. Using the following format .", + "examples" : [ + "my_table.my_vector_column, another_table.vector" + ] } } } diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index a422c05b2553..dbf8d3532fd5 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -94,7 +94,7 @@ def setup_teardown(config: Mapping): "semitechnologies/weaviate:1.16.1", detach=True, environment=env_vars, name=name, ports={8080: ('127.0.0.1', 8081)} ) - time.sleep(1) + time.sleep(0.5) retries = 3 while retries > 0: @@ -169,7 +169,7 @@ def retrieve_all_articles(client: Client) -> List[AirbyteRecordMessage]: def get_objects(client: Client, class_name: str) -> List[Mapping[str, Any]]: """retrieves and formats all Articles as Airbyte messages""" - all_records = client.client.data_object.get(class_name=class_name) + all_records = client.client.data_object.get(class_name=class_name, with_vector=True) return all_records.get("objects") @@ -312,3 +312,31 @@ def test_write_pokemon_source_pikachu(config: Mapping, pokemon_catalog: Configur actual = records_in_destination[0] assert actual["properties"]["name"] == pikachu.record.data.get("name"), "Names should match" + + +def test_upload_vector(config: Mapping, client: Client): + stream_name = "article_with_vector" + stream_schema = {"type": "object", "properties": { + "title": {"type": "string"}, + "vector": {"type": "array", "items": {"type": "number}"}} + }} + catalog = create_catalog(stream_name, stream_schema) + first_state_message = _state({"state": "1"}) + data = {"title": "test1", "vector": [0.1, 0.2]} + first_record_chunk = [_record(stream_name, data)] + + destination = DestinationWeaviate() + config["vectors"] = "article_with_vector.vector" + + expected_states = [first_state_message] + output_states = list( + destination.write( + config, catalog, [*first_record_chunk, first_state_message] + ) + ) + assert expected_states == output_states, "Checkpoint state messages were expected from the destination" + + class_name = "Article_With_Vector" + assert count_objects(client, class_name) == 1, "There should be only 1 object of in Weaviate" + actual = get_objects(client, class_name)[0] + assert actual.get("vector") == data.get("vector"), "Vectors should match" diff --git a/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py b/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py index dddaea0060fa..9f0d78547f1f 100644 --- a/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py @@ -1,7 +1,16 @@ # # Copyright (c) 2022 Airbyte, Inc., all rights reserved. # +from unittest.mock import Mock +from destination_weaviate.client import Client -def test_example_method(): - assert True +def test_client_custom_vectors_config(): + mock_object = Client + mock_object.get_weaviate_client = Mock(return_value=None) + c = Client({"vectors": "my_table.test", "url": "http://test"}, schema={}) + assert c.vectors["my_table"] == "test", "Single vector should work" + + c = Client({"vectors": "case2.test, another_table.vector", "url": "http://test"}, schema={}) + assert c.vectors["case2"] == "test", "Multiple values case2 should work too" + assert c.vectors["another_table"] == "vector", "Multiple values another_table should work too" From 54d08c859b19b4e51abb8d44854bdcbbabc01893 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Wed, 14 Dec 2022 14:31:30 -0800 Subject: [PATCH 20/42] Update docs --- docs/integrations/destinations/weaviate.md | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/docs/integrations/destinations/weaviate.md b/docs/integrations/destinations/weaviate.md index 5cb462124b1b..11447304f1e7 100644 --- a/docs/integrations/destinations/weaviate.md +++ b/docs/integrations/destinations/weaviate.md @@ -8,19 +8,30 @@ | Incremental - Append Sync | Yes | | | Incremental - Deduped History | No | | | Namespaces | No | | -| Provide vector | No | | +| Provide vector | Yes | | #### Output Schema Each stream will be output into its own class in Weaviate. The record fields will be stored as fields in the Weaviate class. +**Uploading Vectors:** Use the vectors configuration if you want to upload +vectors from a source database into Weaviate. You can do this by specifying +the stream name and vector field name in the following format: +``` +., . +``` +For example, if you have a table named `my_table` and the vector is stored using the column `vector` then +you should use the following `vectors`configuration: `my_table.vector`. + Dynamic Schema: Weaviate will automatically create a schema for the stream if no class was defined unless you have disabled the Dynamic Schema feature in Weaviate. You can also create the class in Weaviate in advance if you need more control over the schema in Weaviate. IDs: If your source table has an int based id stored as field name `id` then the -ID will automatically be converted to a UUID. Weaviate only supports ID to be a UUID. +ID will automatically be converted to a UUID. Weaviate only supports the ID to be a UUID. +For example, if the record has `id=1` then this would become a uuid of +`00000000-0000-0000-0000-000000000001`. Any field name starting with an upper case letter will be converted to lower case. For example, if you have a field name `USD` then that field will become `uSD`. This is due to a limitation @@ -53,8 +64,11 @@ You need a Weaviate user or use a Weaviate instance that's accessible to all You should now have all the requirements needed to configure Weaviate as a destination in the UI. You'll need the following information to configure the Weaviate destination: * **URL** for example http://localhost:8080 or https://my-wcs.semi.network -* **Username** -* **Password** +* **Username** (Optional) +* **Password** (Optional) +* **Batch Size** (Optional, defaults to 100) +* **Vectors** a comma separated list of `` to specify the field + names that contain vectors ## Changelog From 5a935b8e90c9a75933feda6adbe3ec53cebf9cda Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Wed, 14 Dec 2022 14:42:34 -0800 Subject: [PATCH 21/42] Add test for existing Weaviate class --- .../integration_tests/integration_test.py | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index dbf8d3532fd5..23294b72f0ce 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -151,7 +151,7 @@ def _record_with_id(stream: str, title: str, word_count: int, id: int) -> Airbyt type=Type.RECORD, record=AirbyteRecordMessage(stream=stream, data={ "title": title, "wordCount": word_count, - "id": id + "id": id }, emitted_at=0) ) @@ -340,3 +340,38 @@ def test_upload_vector(config: Mapping, client: Client): assert count_objects(client, class_name) == 1, "There should be only 1 object of in Weaviate" actual = get_objects(client, class_name)[0] assert actual.get("vector") == data.get("vector"), "Vectors should match" + + +def test_weaviate_existing_class(config: Mapping, client: Client): + class_obj = { + "class": "Article", + "properties": [ + {"dataType": ["string"], "name": "title"}, + {"dataType": ["text"], "name": "content"} + ] + } + client.client.schema.create_class(class_obj) + stream_name = "article" + stream_schema = {"type": "object", "properties": { + "title": {"type": "string"}, + "text": {"type": "string"} + }} + catalog = create_catalog(stream_name, stream_schema) + first_state_message = _state({"state": "1"}) + data = {"title": "test1", "content": "test 1 content"} + first_record_chunk = [_record(stream_name, data)] + + destination = DestinationWeaviate() + expected_states = [first_state_message] + output_states = list( + destination.write( + config, catalog, [*first_record_chunk, first_state_message] + ) + ) + assert expected_states == output_states, "Checkpoint state messages were expected from the destination" + + class_name = stream_name[0].upper() + stream_name[1:] + assert count_objects(client, class_name) == 1, "There should be only 1 object of in Weaviate" + actual = get_objects(client, class_name)[0] + assert actual["properties"].get("title") == data.get("title"), "Title should match" + assert actual["properties"].get("content") == data.get("content"), "Content should match" From 4c1a46e07bf933b24f406e9e50e295011979e535 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Wed, 14 Dec 2022 14:54:44 -0800 Subject: [PATCH 22/42] Add trying to create schema in check connection --- .../destination_weaviate/destination.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py index af82b3c23ddd..fefaac79efad 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py @@ -1,9 +1,9 @@ # # Copyright (c) 2022 Airbyte, Inc., all rights reserved. # - - +import string from typing import Any, Iterable, Mapping +import random from airbyte_cdk import AirbyteLogger import logging @@ -84,6 +84,9 @@ def check(self, logger: AirbyteLogger, config: Mapping[str, Any]) -> AirbyteConn try: client = Client.get_weaviate_client(config) ready = client.is_ready() + class_name = ''.join(random.choices(string.ascii_uppercase, k=10)) + client.schema.create_class({"class": class_name}) + client.schema.delete_class(class_name) if not ready: return AirbyteConnectionStatus(status=Status.FAILED, message=f"Weaviate server {config.get('url')} not ready") return AirbyteConnectionStatus(status=Status.SUCCEEDED) From 2da61706a86caa816537683719c1fb95cbda83cb Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Thu, 15 Dec 2022 11:44:45 -0800 Subject: [PATCH 23/42] Add support for mongodb _id fields --- .../destination_weaviate/client.py | 34 +++++++++++++++---- .../destination_weaviate/spec.json | 5 +-- .../integration_tests/integration_test.py | 29 ++++++++++++++++ 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index 19402d05fef1..36d1198d973d 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -22,6 +22,23 @@ def parse_vectors(vectors_config: str) -> Mapping[str, str]: return vectors +def hex_to_int(hex_str: str) -> int: + try: + return int(hex_str, 16) + except ValueError: + return 0 + + + +def generate_id(record_id: Any) -> uuid.UUID: + if isinstance(record_id, int): + return uuid.UUID(int=record_id) + if isinstance(record_id, str): + id_int = hex_to_int(record_id) + if hex_to_int(record_id) > 0: + return uuid.UUID(int=id_int) + + class Client: def __init__(self, config: Mapping[str, Any], schema: Mapping[str, str]): self.client = self.get_weaviate_client(config) @@ -33,14 +50,16 @@ def __init__(self, config: Mapping[str, Any], schema: Mapping[str, str]): def queue_write_operation(self, stream_name: str, record: Mapping): # TODO need to handle case where original DB ID is not a UUID - id = "" - if record.get("id"): - id = record.get("id") - if isinstance(id, int): - id = uuid.UUID(int=id) + record_id = "" + if "id" in record: + record_id = generate_id(record.get("id")) del record["id"] + # Weaviate will throw an error if you try to store a field with name _id + elif "_id" in record: + record_id = generate_id(record.get("_id")) + del record["_id"] else: - id = uuid.uuid4() + record_id = uuid.uuid4() # TODO support nested objects instead of converting to json string when weaviate supports this for k, v in record.items(): @@ -50,6 +69,7 @@ def queue_write_operation(self, stream_name: str, record: Mapping): if isinstance(v, list) and len(v) == 0 and k not in self.schema[stream_name]: record[k] = "" + # Property names in Weaviate have to start with lowercase letter record = {k[0].lower() + k[1:]: v for k, v in record.items()} vector = None @@ -57,7 +77,7 @@ def queue_write_operation(self, stream_name: str, record: Mapping): vector_column_name = self.vectors.get(stream_name) vector = record.get(vector_column_name) del record[vector_column_name] - self.client.batch.add_data_object(record, stream_name.title(), id, vector=vector) + self.client.batch.add_data_object(record, stream_name.title(), record_id, vector=vector) if self.client.batch.num_objects() >= self.batch_size: self.flush() diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json index da8c9c6201b8..a07e54ac9383 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json @@ -41,9 +41,10 @@ }, "vectors" : { "type" : "string", - "description" : "Comma separated list of strings to configure which field holds the vector foreach stream. Using the following format .", + "description" : "Comma separated list of strings of `stream_name.vector_column_name` to specify which field holds the vectors.", "examples" : [ - "my_table.my_vector_column, another_table.vector" + "my_table.my_vector_column, another_table.vector", + "mytable.vector" ] } } diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index 23294b72f0ce..b2a1e26d8f7d 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -5,6 +5,7 @@ import json import logging import time +import uuid from typing import Any, Dict, List, Mapping import os @@ -375,3 +376,31 @@ def test_weaviate_existing_class(config: Mapping, client: Client): actual = get_objects(client, class_name)[0] assert actual["properties"].get("title") == data.get("title"), "Title should match" assert actual["properties"].get("content") == data.get("content"), "Content should match" + + +def test_id_starting_with_underscore(config: Mapping, client: Client): + # This is common scenario from mongoDB + stream_name = "article" + stream_schema = {"type": "object", "properties": { + "_id": {"type": "integer"}, + "title": {"type": "string"} + }} + catalog = create_catalog(stream_name, stream_schema) + first_state_message = _state({"state": "1"}) + data = {"_id": "507f191e810c19729de860ea", "title": "test1"} + first_record_chunk = [_record(stream_name, data)] + + destination = DestinationWeaviate() + + expected_states = [first_state_message] + output_states = list( + destination.write( + config, catalog, [*first_record_chunk, first_state_message] + ) + ) + assert expected_states == output_states, "Checkpoint state messages were expected from the destination" + + class_name = stream_name[0].upper() + stream_name[1:] + assert count_objects(client, class_name) == 1, "There should be only 1 object of in Weaviate" + actual = get_objects(client, class_name)[0] + assert actual.get("id") == str(uuid.UUID(int=int(data.get("_id"), 16))), "UUID should be created for _id field" From 1915b6e85d7a19f1dbc052e78807f4cb22245dbc Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Fri, 16 Dec 2022 09:53:44 -0800 Subject: [PATCH 24/42] Add support for providing custom ID --- .../destination_weaviate/client.py | 36 ++++++++++++++----- .../destination_weaviate/spec.json | 8 +++++ .../integration_tests/integration_test.py | 29 +++++++++++++++ .../unit_tests/unit_test.py | 12 +++++++ docs/integrations/destinations/weaviate.md | 9 ++--- 5 files changed, 81 insertions(+), 13 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index 36d1198d973d..3b636655a3e1 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -22,6 +22,18 @@ def parse_vectors(vectors_config: str) -> Mapping[str, str]: return vectors +def parse_id_schema(id_schema_config: str) -> Mapping[str, str]: + id_schema = {} + if not id_schema_config: + return id_schema + + id_schema_list = id_schema_config.replace(" ", "").split(",") + for schema_id in id_schema_list: + stream_name, id_field_name = schema_id.split(".") + id_schema[stream_name] = id_field_name + return id_schema + + def hex_to_int(hex_str: str) -> int: try: return int(hex_str, 16) @@ -46,20 +58,26 @@ def __init__(self, config: Mapping[str, Any], schema: Mapping[str, str]): self.batch_size = int(config.get("batch_size", 100)) self.schema = schema self.vectors = parse_vectors(config.get("vectors")) - + self.id_schema = parse_id_schema(config.get("id_schema")) def queue_write_operation(self, stream_name: str, record: Mapping): # TODO need to handle case where original DB ID is not a UUID record_id = "" - if "id" in record: - record_id = generate_id(record.get("id")) - del record["id"] - # Weaviate will throw an error if you try to store a field with name _id - elif "_id" in record: - record_id = generate_id(record.get("_id")) - del record["_id"] + if self.id_schema.get(stream_name, "") in record: + id_field_name = self.id_schema.get(stream_name, "") + record_id = generate_id(record.get(id_field_name)) + del record[id_field_name] + print("handling user provided id schema") else: - record_id = uuid.uuid4() + if "id" in record: + record_id = generate_id(record.get("id")) + del record["id"] + # Weaviate will throw an error if you try to store a field with name _id + elif "_id" in record: + record_id = generate_id(record.get("_id")) + del record["_id"] + else: + record_id = uuid.uuid4() # TODO support nested objects instead of converting to json string when weaviate supports this for k, v in record.items(): diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json index a07e54ac9383..78304b2a3329 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json @@ -46,6 +46,14 @@ "my_table.my_vector_column, another_table.vector", "mytable.vector" ] + }, + "id_schema" : { + "type" : "string", + "description" : "Comma separated list of strings of `stream_name.id_column_name` to specify which field holds the ID of the record.", + "examples" : [ + "my_table.my_id_column, another_table.id", + "users.user_id" + ] } } } diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index b2a1e26d8f7d..400a97e2282a 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -404,3 +404,32 @@ def test_id_starting_with_underscore(config: Mapping, client: Client): assert count_objects(client, class_name) == 1, "There should be only 1 object of in Weaviate" actual = get_objects(client, class_name)[0] assert actual.get("id") == str(uuid.UUID(int=int(data.get("_id"), 16))), "UUID should be created for _id field" + + +def test_id_custom_field_name(config: Mapping, client: Client): + # This is common scenario from mongoDB + stream_name = "article" + stream_schema = {"type": "object", "properties": { + "my_id": {"type": "integer"}, + "title": {"type": "string"} + }} + catalog = create_catalog(stream_name, stream_schema) + first_state_message = _state({"state": "1"}) + data = {"my_id": "507f191e810c19729de860ea", "title": "test_id_schema"} + first_record_chunk = [_record(stream_name, data)] + + destination = DestinationWeaviate() + config["id_schema"] = "article.my_id" + + expected_states = [first_state_message] + output_states = list( + destination.write( + config, catalog, [*first_record_chunk, first_state_message] + ) + ) + assert expected_states == output_states, "Checkpoint state messages were expected from the destination" + + class_name = stream_name[0].upper() + stream_name[1:] + assert count_objects(client, class_name) == 1, "There should be only 1 object of in Weaviate" + actual = get_objects(client, class_name)[0] + assert actual.get("id") == str(uuid.UUID(int=int(data.get("my_id"), 16))), "UUID should be created for my_id field" diff --git a/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py b/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py index 9f0d78547f1f..581fa87e7f6e 100644 --- a/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py @@ -5,6 +5,7 @@ from destination_weaviate.client import Client + def test_client_custom_vectors_config(): mock_object = Client mock_object.get_weaviate_client = Mock(return_value=None) @@ -14,3 +15,14 @@ def test_client_custom_vectors_config(): c = Client({"vectors": "case2.test, another_table.vector", "url": "http://test"}, schema={}) assert c.vectors["case2"] == "test", "Multiple values case2 should work too" assert c.vectors["another_table"] == "vector", "Multiple values another_table should work too" + + +def test_client_custom_id_schema_config(): + mock_object = Client + mock_object.get_weaviate_client = Mock(return_value=None) + c = Client({"id_schema": "my_table.my_id", "url": "http://test"}, schema={}) + assert c.id_schema["my_table"] == "my_id", "Single id_schema definition should work" + + c = Client({"id_schema": "my_table.my_id, another_table.my_id2", "url": "http://test"}, schema={}) + assert c.id_schema["my_table"] == "my_id", "Multiple values should work too" + assert c.id_schema["another_table"] == "my_id2", "Multiple values should work too" diff --git a/docs/integrations/destinations/weaviate.md b/docs/integrations/destinations/weaviate.md index 11447304f1e7..b7e21dd9d7e3 100644 --- a/docs/integrations/destinations/weaviate.md +++ b/docs/integrations/destinations/weaviate.md @@ -46,7 +46,7 @@ password. #### Requirements -To use the ClickHouse destination, you'll need: +To use the Weaviate destination, you'll need: * A Weaviate cluster version 21.8.10.19 or above @@ -59,7 +59,7 @@ Make sure your Weaviate database can be accessed by Airbyte. If your database is You need a Weaviate user or use a Weaviate instance that's accessible to all -### Setup the ClickHouse Destination in Airbyte +### Setup the Weaviate Destination in Airbyte You should now have all the requirements needed to configure Weaviate as a destination in the UI. You'll need the following information to configure the Weaviate destination: @@ -68,12 +68,13 @@ You should now have all the requirements needed to configure Weaviate as a desti * **Password** (Optional) * **Batch Size** (Optional, defaults to 100) * **Vectors** a comma separated list of `` to specify the field - names that contain vectors +* **ID Schema** a comma separated list of `` to specify the field + name that contains the ID of a record ## Changelog | Version | Date | Pull Request | Subject | |:--------|:-----------| :--- |:---------------------------------------------| -| 0.1.0 | 2021-11-04 | [\#20094](https://github.com/airbytehq/airbyte/pull/20094) | Add ClickHouse destination | +| 0.1.0 | 2022-12-06 | [\#20094](https://github.com/airbytehq/airbyte/pull/20094) | Add Weaviate destination | From 9173d23855648259f1362547ca87837609a2e864 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Fri, 16 Dec 2022 10:27:00 -0800 Subject: [PATCH 25/42] remove unused file --- .../destination-weaviate/destination_weaviate/writer.py | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 airbyte-integrations/connectors/destination-weaviate/destination_weaviate/writer.py diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/writer.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/writer.py deleted file mode 100644 index afff7653e69c..000000000000 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/writer.py +++ /dev/null @@ -1,5 +0,0 @@ -# -# Copyright (c) 2022 Airbyte, Inc., all rights reserved. -# - -# From 28f364982cdc24ba8acffd2dcec43a8bbda0e289 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Fri, 16 Dec 2022 11:46:52 -0800 Subject: [PATCH 26/42] fix flow of is_ready() check --- .../destination-weaviate/destination_weaviate/destination.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py index fefaac79efad..e9b2663d686c 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py @@ -84,11 +84,12 @@ def check(self, logger: AirbyteLogger, config: Mapping[str, Any]) -> AirbyteConn try: client = Client.get_weaviate_client(config) ready = client.is_ready() + if not ready: + return AirbyteConnectionStatus(status=Status.FAILED, message=f"Weaviate server {config.get('url')} not ready") + class_name = ''.join(random.choices(string.ascii_uppercase, k=10)) client.schema.create_class({"class": class_name}) client.schema.delete_class(class_name) - if not ready: - return AirbyteConnectionStatus(status=Status.FAILED, message=f"Weaviate server {config.get('url')} not ready") return AirbyteConnectionStatus(status=Status.SUCCEEDED) except Exception as e: return AirbyteConnectionStatus(status=Status.FAILED, message=f"An exception occurred: {repr(e)}") From f190fc9e261731a80e5641372b77381b688b3a0f Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Fri, 16 Dec 2022 11:58:10 -0800 Subject: [PATCH 27/42] Move standalone functions to utils.py --- .../destination_weaviate/client.py | 44 +------------- .../destination_weaviate/destination.py | 20 +------ .../destination_weaviate/utils.py | 58 +++++++++++++++++++ 3 files changed, 62 insertions(+), 60 deletions(-) create mode 100644 airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index 3b636655a3e1..29e250a5d3d0 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -5,50 +5,10 @@ import uuid import logging import json -from typing import Any, Mapping, List +from typing import Any, Mapping import weaviate - - -def parse_vectors(vectors_config: str) -> Mapping[str, str]: - vectors = {} - if not vectors_config: - return vectors - - vectors_list = vectors_config.replace(" ", "").split(",") - for vector in vectors_list: - stream_name, vector_column_name = vector.split(".") - vectors[stream_name] = vector_column_name - return vectors - - -def parse_id_schema(id_schema_config: str) -> Mapping[str, str]: - id_schema = {} - if not id_schema_config: - return id_schema - - id_schema_list = id_schema_config.replace(" ", "").split(",") - for schema_id in id_schema_list: - stream_name, id_field_name = schema_id.split(".") - id_schema[stream_name] = id_field_name - return id_schema - - -def hex_to_int(hex_str: str) -> int: - try: - return int(hex_str, 16) - except ValueError: - return 0 - - - -def generate_id(record_id: Any) -> uuid.UUID: - if isinstance(record_id, int): - return uuid.UUID(int=record_id) - if isinstance(record_id, str): - id_int = hex_to_int(record_id) - if hex_to_int(record_id) > 0: - return uuid.UUID(int=id_int) +from .utils import generate_id, parse_id_schema, parse_vectors class Client: diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py index e9b2663d686c..a35492a097bf 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py @@ -6,34 +6,18 @@ import random from airbyte_cdk import AirbyteLogger -import logging from airbyte_cdk.destinations import Destination from airbyte_cdk.models import AirbyteConnectionStatus, AirbyteMessage, ConfiguredAirbyteCatalog, Status, Type from .client import Client - - -def get_schema(configured_catalog: ConfiguredAirbyteCatalog) -> Mapping[str, Mapping[str, str]]: - schema = {} - for stream in configured_catalog.streams: - stream_schema = {} - for k, v in stream.stream.json_schema.get("properties").items(): - stream_schema[k] = "default" - if "array" in v.get("type", []) and "object" in v.get("items", {}).get("type", []): - stream_schema[k] = "jsonify" - if "object" in v.get("type", []): - stream_schema[k] = "jsonify" - schema[stream.stream.name] = stream_schema - return schema +from .utils import get_schema_from_catalog class DestinationWeaviate(Destination): def write( self, config: Mapping[str, Any], configured_catalog: ConfiguredAirbyteCatalog, input_messages: Iterable[AirbyteMessage] ) -> Iterable[AirbyteMessage]: - """ - TODO Reads the input stream of messages, config, and catalog to write data to the destination. This method returns an iterable (typically a generator of AirbyteMessages via yield) containing state messages received @@ -47,7 +31,7 @@ def write( :param input_messages: The stream of input messages received from the source :return: Iterable of AirbyteStateMessages wrapped in AirbyteMessage structs """ - client = Client(config, get_schema(configured_catalog)) + client = Client(config, get_schema_from_catalog(configured_catalog)) # TODO add support for overwrite mode # for configured_stream in configured_catalog.streams: # if configured_stream.destination_sync_mode == DestinationSyncMode.overwrite: diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py new file mode 100644 index 000000000000..35a128e4924b --- /dev/null +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py @@ -0,0 +1,58 @@ +from typing import Mapping, Any +import uuid + +from airbyte_cdk.models import ConfiguredAirbyteCatalog + + +def parse_vectors(vectors_config: str) -> Mapping[str, str]: + vectors = {} + if not vectors_config: + return vectors + + vectors_list = vectors_config.replace(" ", "").split(",") + for vector in vectors_list: + stream_name, vector_column_name = vector.split(".") + vectors[stream_name] = vector_column_name + return vectors + + +def parse_id_schema(id_schema_config: str) -> Mapping[str, str]: + id_schema = {} + if not id_schema_config: + return id_schema + + id_schema_list = id_schema_config.replace(" ", "").split(",") + for schema_id in id_schema_list: + stream_name, id_field_name = schema_id.split(".") + id_schema[stream_name] = id_field_name + return id_schema + + +def hex_to_int(hex_str: str) -> int: + try: + return int(hex_str, 16) + except ValueError: + return 0 + + +def generate_id(record_id: Any) -> uuid.UUID: + if isinstance(record_id, int): + return uuid.UUID(int=record_id) + if isinstance(record_id, str): + id_int = hex_to_int(record_id) + if hex_to_int(record_id) > 0: + return uuid.UUID(int=id_int) + + +def get_schema_from_catalog(configured_catalog: ConfiguredAirbyteCatalog) -> Mapping[str, Mapping[str, str]]: + schema = {} + for stream in configured_catalog.streams: + stream_schema = {} + for k, v in stream.stream.json_schema.get("properties").items(): + stream_schema[k] = "default" + if "array" in v.get("type", []) and "object" in v.get("items", {}).get("type", []): + stream_schema[k] = "jsonify" + if "object" in v.get("type", []): + stream_schema[k] = "jsonify" + schema[stream.stream.name] = stream_schema + return schema From f0618e629d786f00812a0d5f2cd065273687eccb Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Fri, 16 Dec 2022 13:50:39 -0800 Subject: [PATCH 28/42] Support overwrite mode --- .../destination_weaviate/client.py | 19 ++++- .../destination_weaviate/destination.py | 9 +- .../destination_weaviate/spec.json | 3 +- .../destination_weaviate/utils.py | 5 ++ .../integration_tests/integration_test.py | 82 ++++++++++++++++--- 5 files changed, 98 insertions(+), 20 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index 29e250a5d3d0..b86299529f78 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -8,7 +8,7 @@ from typing import Any, Mapping import weaviate -from .utils import generate_id, parse_id_schema, parse_vectors +from .utils import generate_id, parse_id_schema, parse_vectors, stream_to_class_name class Client: @@ -55,7 +55,8 @@ def queue_write_operation(self, stream_name: str, record: Mapping): vector_column_name = self.vectors.get(stream_name) vector = record.get(vector_column_name) del record[vector_column_name] - self.client.batch.add_data_object(record, stream_name.title(), record_id, vector=vector) + class_name = stream_to_class_name(stream_name) + self.client.batch.add_data_object(record, class_name, record_id, vector=vector) if self.client.batch.num_objects() >= self.batch_size: self.flush() @@ -67,6 +68,20 @@ def flush(self): if errors: logging.error(f"Object {result.get('id')} had errors: {errors}") + def delete_stream_entries(self, stream_name: str): + class_name = stream_to_class_name(stream_name) + try: + original_schema = self.client.schema.get(class_name=class_name) + self.client.schema.delete_class(class_name=class_name) + logging.info(f"Deleted class {class_name}") + self.client.schema.create_class(original_schema) + logging.info(f"Recreated class {class_name}") + except weaviate.exceptions.UnexpectedStatusCodeException as e: + if e.message.startswith("Get schema! Unexpected status code: 404"): + logging.info(f"Class {class_name} did not exist.") + else: + raise e + @staticmethod def get_weaviate_client(config: Mapping[str, Any]) -> weaviate.Client: url, username, password = config.get("url"), config.get("username"), config.get("password") diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py index a35492a097bf..5baea306ed33 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py @@ -7,7 +7,7 @@ from airbyte_cdk import AirbyteLogger from airbyte_cdk.destinations import Destination -from airbyte_cdk.models import AirbyteConnectionStatus, AirbyteMessage, ConfiguredAirbyteCatalog, Status, Type +from airbyte_cdk.models import AirbyteConnectionStatus, AirbyteMessage, ConfiguredAirbyteCatalog, DestinationSyncMode, Status, Type from .client import Client from .utils import get_schema_from_catalog @@ -32,10 +32,9 @@ def write( :return: Iterable of AirbyteStateMessages wrapped in AirbyteMessage structs """ client = Client(config, get_schema_from_catalog(configured_catalog)) - # TODO add support for overwrite mode - # for configured_stream in configured_catalog.streams: - # if configured_stream.destination_sync_mode == DestinationSyncMode.overwrite: - # client.delete_stream_entries(configured_stream.stream.name) + for configured_stream in configured_catalog.streams: + if configured_stream.destination_sync_mode == DestinationSyncMode.overwrite: + client.delete_stream_entries(configured_stream.stream.name) for message in input_messages: if message.type == Type.STATE: diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json index 78304b2a3329..762464d35756 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json @@ -1,7 +1,8 @@ { "documentationUrl" : "https://docs.airbyte.com/integrations/destinations/weaviate", "supported_destination_sync_modes" : [ - "append" + "append", + "overwrite" ], "supportsIncremental" : true, "supportsDBT" : false, diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py index 35a128e4924b..d132887e5e5c 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py @@ -56,3 +56,8 @@ def get_schema_from_catalog(configured_catalog: ConfiguredAirbyteCatalog) -> Map stream_schema[k] = "jsonify" schema[stream.stream.name] = stream_schema return schema + + +def stream_to_class_name(stream_name: str) -> str: + stream_name = stream_name.replace(" ", "") + return stream_name[0].upper() + stream_name[1:] diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index 400a97e2282a..5c971ee2e940 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -26,6 +26,7 @@ ) from destination_weaviate import DestinationWeaviate from destination_weaviate.client import Client +from destination_weaviate.utils import stream_to_class_name @pytest.fixture(name="config") @@ -34,11 +35,12 @@ def config_fixture() -> Mapping[str, Any]: return json.loads(f.read()) -def create_catalog(stream_name: str, stream_schema: Mapping[str, Any]) -> ConfiguredAirbyteCatalog: +def create_catalog(stream_name: str, stream_schema: Mapping[str, Any], + sync_mode: DestinationSyncMode = DestinationSyncMode.append) -> ConfiguredAirbyteCatalog: append_stream = ConfiguredAirbyteStream( stream=AirbyteStream(name=stream_name, json_schema=stream_schema, supported_sync_modes=[SyncMode.incremental]), sync_mode=SyncMode.incremental, - destination_sync_mode=DestinationSyncMode.append, + destination_sync_mode=sync_mode, ) return ConfiguredAirbyteCatalog(streams=[append_stream]) @@ -182,12 +184,6 @@ def count_objects(client: Client, class_name: str) -> int: def test_write(config: Mapping, article_catalog: ConfiguredAirbyteCatalog, client: Client): - """ - This test verifies that: - TODO: 1. writing a stream in "overwrite" mode overwrites any existing data for that stream - 2. writing a stream in "append" mode appends new records without deleting the old ones - 3. The correct state message is output by the connector at the end of the sync - """ append_stream = article_catalog.streams[0].stream.name first_state_message = _state({"state": "1"}) first_record_chunk = [_article_record(append_stream, str(i), i) for i in range(5)] @@ -207,6 +203,8 @@ def test_write(config: Mapping, article_catalog: ConfiguredAirbyteCatalog, clien assert expected_records == records_in_destination, "Records in destination should match records expected" + + def test_write_large_batch(config: Mapping, article_catalog: ConfiguredAirbyteCatalog, client: Client): append_stream = article_catalog.streams[0].stream.name first_state_message = _state({"state": "1"}) @@ -337,7 +335,7 @@ def test_upload_vector(config: Mapping, client: Client): ) assert expected_states == output_states, "Checkpoint state messages were expected from the destination" - class_name = "Article_With_Vector" + class_name = stream_to_class_name(stream_name) assert count_objects(client, class_name) == 1, "There should be only 1 object of in Weaviate" actual = get_objects(client, class_name)[0] assert actual.get("vector") == data.get("vector"), "Vectors should match" @@ -371,7 +369,7 @@ def test_weaviate_existing_class(config: Mapping, client: Client): ) assert expected_states == output_states, "Checkpoint state messages were expected from the destination" - class_name = stream_name[0].upper() + stream_name[1:] + class_name = stream_to_class_name(stream_name) assert count_objects(client, class_name) == 1, "There should be only 1 object of in Weaviate" actual = get_objects(client, class_name)[0] assert actual["properties"].get("title") == data.get("title"), "Title should match" @@ -400,7 +398,7 @@ def test_id_starting_with_underscore(config: Mapping, client: Client): ) assert expected_states == output_states, "Checkpoint state messages were expected from the destination" - class_name = stream_name[0].upper() + stream_name[1:] + class_name = stream_to_class_name(stream_name) assert count_objects(client, class_name) == 1, "There should be only 1 object of in Weaviate" actual = get_objects(client, class_name)[0] assert actual.get("id") == str(uuid.UUID(int=int(data.get("_id"), 16))), "UUID should be created for _id field" @@ -429,7 +427,67 @@ def test_id_custom_field_name(config: Mapping, client: Client): ) assert expected_states == output_states, "Checkpoint state messages were expected from the destination" - class_name = stream_name[0].upper() + stream_name[1:] + class_name = stream_to_class_name(stream_name) assert count_objects(client, class_name) == 1, "There should be only 1 object of in Weaviate" actual = get_objects(client, class_name)[0] assert actual.get("id") == str(uuid.UUID(int=int(data.get("my_id"), 16))), "UUID should be created for my_id field" + + +def test_write_overwrite(config: Mapping, client: Client): + stream_name = "article" + stream_schema = {"type": "object", "properties": { + "title": {"type": "string"}, + "text": {"type": "string"} + }} + catalog = create_catalog(stream_name, stream_schema, sync_mode=DestinationSyncMode.overwrite) + first_state_message = _state({"state": "1"}) + data = {"title": "test1", "content": "test 1 content"} + first_record_chunk = [_record(stream_name, data), _record(stream_name, data)] + + destination = DestinationWeaviate() + expected_states = [first_state_message] + output_states = list( + destination.write( + config, catalog, [*first_record_chunk, first_state_message] + ) + ) + assert expected_states == output_states, "Checkpoint state messages were expected from the destination" + class_name = stream_to_class_name(stream_name) + assert count_objects(client, class_name) == 2 + + # After writing a 2nd time the existing 2 objects should be gone and there should only be 1 new object + second_state_message = _state({"state": "2"}) + second_record_chunk = [_record(stream_name, data)] + expected_states = [second_state_message] + output_states = list( + destination.write( + config, catalog, [*second_record_chunk, second_state_message] + ) + ) + + assert expected_states == output_states, "Checkpoint state messages were expected from the destination" + assert count_objects(client, class_name) == 1 + + +def test_client_delete_stream_entries(caplog, client: Client): + client.delete_stream_entries("doesnotexist") + assert "Class Doesnotexist did not exist." in caplog.text, "Should be a log entry that says class doesn't exist" + + class_obj = { + "class": "Article", + "properties": [ + {"dataType": ["string"], "name": "title", "moduleConfig": { + "text2vec-contextionary": { + "vectorizePropertyName": True + } + }}, + {"dataType": ["text"], "name": "content"} + ] + } + client.client.schema.create_class(class_obj) + client.client.data_object.create({"title": "test-deleted", "content": "test-deleted"}, "Article") + client.delete_stream_entries("article") + assert count_objects(client, "Article") == 0, "Ensure articles have been deleted however class was recreated" + actual_schema = client.client.schema.get("Article") + title_prop = next(filter(lambda x: x["name"] == "title", actual_schema["properties"])) + assert title_prop["moduleConfig"]["text2vec-contextionary"]["vectorizePropertyName"] == True, "Ensure moduleconfig is persisted" From 46d9525caced2c657e6abca28ce7c2c86495526b Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Sat, 17 Dec 2022 07:48:48 -0800 Subject: [PATCH 29/42] Add regex based stream_name_class_name conversion --- .../destination-weaviate/destination_weaviate/utils.py | 3 +++ .../destination-weaviate/unit_tests/unit_test.py | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py index d132887e5e5c..c242eec91e3a 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py @@ -1,5 +1,6 @@ from typing import Mapping, Any import uuid +import re from airbyte_cdk.models import ConfiguredAirbyteCatalog @@ -59,5 +60,7 @@ def get_schema_from_catalog(configured_catalog: ConfiguredAirbyteCatalog) -> Map def stream_to_class_name(stream_name: str) -> str: + pattern = "[^0-9A-Za-z_]+" + stream_name = re.sub(pattern, "", stream_name) stream_name = stream_name.replace(" ", "") return stream_name[0].upper() + stream_name[1:] diff --git a/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py b/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py index 581fa87e7f6e..79eec344f90e 100644 --- a/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py @@ -4,6 +4,7 @@ from unittest.mock import Mock from destination_weaviate.client import Client +from destination_weaviate.utils import stream_to_class_name def test_client_custom_vectors_config(): @@ -26,3 +27,11 @@ def test_client_custom_id_schema_config(): c = Client({"id_schema": "my_table.my_id, another_table.my_id2", "url": "http://test"}, schema={}) assert c.id_schema["my_table"] == "my_id", "Multiple values should work too" assert c.id_schema["another_table"] == "my_id2", "Multiple values should work too" + + +def test_utils_stream_name_to_class_name(): + assert stream_to_class_name("s-a") == "Sa" + assert stream_to_class_name("s_a") == "S_a" + assert stream_to_class_name("s _ a") == "S_a" + assert stream_to_class_name("s{} _ a") == "S_a" + assert stream_to_class_name("s{} _ aA") == "S_aA" From b2d1fa3d0548b448837b9e114986e53998f2d04f Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Mon, 19 Dec 2022 14:46:57 -0800 Subject: [PATCH 30/42] remove unneeded print statement --- .../destination-weaviate/destination_weaviate/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index b86299529f78..56294ed2696f 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -27,7 +27,6 @@ def queue_write_operation(self, stream_name: str, record: Mapping): id_field_name = self.id_schema.get(stream_name, "") record_id = generate_id(record.get(id_field_name)) del record[id_field_name] - print("handling user provided id schema") else: if "id" in record: record_id = generate_id(record.get("id")) From 85aeae8e0315df4b21b7a50d6a02e3859e073761 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Mon, 19 Dec 2022 15:36:38 -0800 Subject: [PATCH 31/42] Add "airbyte_secret" : true to password config --- .../destination-weaviate/destination_weaviate/spec.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json index 762464d35756..eb21a5795318 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/spec.json @@ -33,7 +33,8 @@ }, "password" : { "type" : "string", - "description" : "Password used with OIDC authentication" + "description" : "Password used with OIDC authentication", + "airbyte_secret" : true }, "batch_size" : { "type" : "integer", From 83b3a09fcb67b9626a085dedd9161a100627b347 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Mon, 19 Dec 2022 15:47:50 -0800 Subject: [PATCH 32/42] add support for array of arrays --- .../destination-weaviate/destination_weaviate/client.py | 1 - .../destination-weaviate/destination_weaviate/utils.py | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index 56294ed2696f..0a13dc3bd592 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -46,7 +46,6 @@ def queue_write_operation(self, stream_name: str, record: Mapping): if isinstance(v, list) and len(v) == 0 and k not in self.schema[stream_name]: record[k] = "" - # Property names in Weaviate have to start with lowercase letter record = {k[0].lower() + k[1:]: v for k, v in record.items()} vector = None diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py index c242eec91e3a..8b25b593427d 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py @@ -51,7 +51,9 @@ def get_schema_from_catalog(configured_catalog: ConfiguredAirbyteCatalog) -> Map stream_schema = {} for k, v in stream.stream.json_schema.get("properties").items(): stream_schema[k] = "default" - if "array" in v.get("type", []) and "object" in v.get("items", {}).get("type", []): + if "array" in v.get("type", []) and ( + "object" in v.get("items", {}).get("type", []) or + "array" in v.get("items", {}).get("type", [])): stream_schema[k] = "jsonify" if "object" in v.get("type", []): stream_schema[k] = "jsonify" From 0d4e2306aa9001ecd7f5da125e78cc6a2f7bb09c Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Mon, 19 Dec 2022 16:25:44 -0800 Subject: [PATCH 33/42] remove unneeded variable declaration --- .../destination-weaviate/destination_weaviate/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index 0a13dc3bd592..b55e0e62ab8a 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -22,7 +22,6 @@ def __init__(self, config: Mapping[str, Any], schema: Mapping[str, str]): def queue_write_operation(self, stream_name: str, record: Mapping): # TODO need to handle case where original DB ID is not a UUID - record_id = "" if self.id_schema.get(stream_name, "") in record: id_field_name = self.id_schema.get(stream_name, "") record_id = generate_id(record.get(id_field_name)) From c71e3be49836137788752282a8a1b6216e9818f5 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Tue, 20 Dec 2022 09:30:15 -0800 Subject: [PATCH 34/42] change to MutableMapping since we use del --- .../destination-weaviate/destination_weaviate/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index b55e0e62ab8a..21c9a4f975d8 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -5,7 +5,7 @@ import uuid import logging import json -from typing import Any, Mapping +from typing import Any, Mapping, MutableMapping import weaviate from .utils import generate_id, parse_id_schema, parse_vectors, stream_to_class_name @@ -20,7 +20,7 @@ def __init__(self, config: Mapping[str, Any], schema: Mapping[str, str]): self.vectors = parse_vectors(config.get("vectors")) self.id_schema = parse_id_schema(config.get("id_schema")) - def queue_write_operation(self, stream_name: str, record: Mapping): + def queue_write_operation(self, stream_name: str, record: MutableMapping): # TODO need to handle case where original DB ID is not a UUID if self.id_schema.get(stream_name, "") in record: id_field_name = self.id_schema.get(stream_name, "") From da2533d70c5a41c5d4dc98dc1c8c25952fae406b Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Tue, 20 Dec 2022 09:32:22 -0800 Subject: [PATCH 35/42] change name from queued_write to buffered_write --- .../destination-weaviate/destination_weaviate/client.py | 2 +- .../destination-weaviate/destination_weaviate/destination.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index 21c9a4f975d8..b525ac22c347 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -20,7 +20,7 @@ def __init__(self, config: Mapping[str, Any], schema: Mapping[str, str]): self.vectors = parse_vectors(config.get("vectors")) self.id_schema = parse_id_schema(config.get("id_schema")) - def queue_write_operation(self, stream_name: str, record: MutableMapping): + def buffered_write_operation(self, stream_name: str, record: MutableMapping): # TODO need to handle case where original DB ID is not a UUID if self.id_schema.get(stream_name, "") in record: id_field_name = self.id_schema.get(stream_name, "") diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py index 5baea306ed33..a1e8be882ad1 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py @@ -44,7 +44,7 @@ def write( yield message elif message.type == Type.RECORD: record = message.record - client.queue_write_operation(record.stream, record.data) + client.buffered_write_operation(record.stream, record.data) else: # ignore other message types for now continue From 02078343f3df1c87e881e9d436618f90e753282e Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Wed, 21 Dec 2022 11:27:22 -0800 Subject: [PATCH 36/42] add retry on partial batch error --- .../destination_weaviate/client.py | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index b525ac22c347..a1956b20e46c 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -5,12 +5,22 @@ import uuid import logging import json -from typing import Any, Mapping, MutableMapping +from dataclasses import dataclass +import time +from typing import Any, Mapping, MutableMapping, List import weaviate from .utils import generate_id, parse_id_schema, parse_vectors, stream_to_class_name +@dataclass +class BufferedObject: + id: str + properties: Mapping[str, Any] + vector: List[Any] + class_name: str + + class Client: def __init__(self, config: Mapping[str, Any], schema: Mapping[str, str]): self.client = self.get_weaviate_client(config) @@ -19,6 +29,7 @@ def __init__(self, config: Mapping[str, Any], schema: Mapping[str, str]): self.schema = schema self.vectors = parse_vectors(config.get("vectors")) self.id_schema = parse_id_schema(config.get("id_schema")) + self.buffered_objects: MutableMapping[str, BufferedObject] = {} def buffered_write_operation(self, stream_name: str, record: MutableMapping): # TODO need to handle case where original DB ID is not a UUID @@ -54,16 +65,34 @@ def buffered_write_operation(self, stream_name: str, record: MutableMapping): del record[vector_column_name] class_name = stream_to_class_name(stream_name) self.client.batch.add_data_object(record, class_name, record_id, vector=vector) + self.buffered_objects[record_id] = BufferedObject(record_id, record, vector, class_name) if self.client.batch.num_objects() >= self.batch_size: self.flush() - def flush(self): - # TODO add error handling instead of just logging + def flush(self, retries: int = 3): results = self.client.batch.create_objects() + objects_with_error = [] for result in results: errors = result.get("result", {}).get("errors", []) if errors: - logging.error(f"Object {result.get('id')} had errors: {errors}") + objects_with_error.append({"id": result.get("id"), "errors": errors}) + logging.info(f"Object {result.get('id')} had errors: {errors}. Going to retry.") + + for object_with_error in objects_with_error: + buffered_object = self.buffered_objects[object_with_error["id"]] + self.client.batch.add_data_object(buffered_object.properties, buffered_object.class_name, buffered_object.id, + buffered_object.vector) + + if objects_with_error and retries > 0: + logging.info("sleeping 2 seconds before retrying batch again") + time.sleep(2) + self.flush(retries - 1) + + if objects_with_error and retries <= 0: + error_msg = f"Objects had errors and retries failed as well: {objects_with_error}" + raise Exception(error_msg) + + self.buffered_objects.clear() def delete_stream_entries(self, stream_name: str): class_name = stream_to_class_name(stream_name) From dc1e7a960b25cca493627749aeb7919af858b62b Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Wed, 21 Dec 2022 11:55:35 -0800 Subject: [PATCH 37/42] Fix partial batch retry and add tests --- .../destination_weaviate/client.py | 9 ++++-- .../destination_weaviate/utils.py | 5 ++++ .../create_objects_partial_error.json | 30 +++++++++++++++++++ .../integration_tests/integration_test.py | 13 +++++++- 4 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 airbyte-integrations/connectors/destination-weaviate/integration_tests/create_objects_partial_error.json diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index a1956b20e46c..90603c101c98 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -21,6 +21,10 @@ class BufferedObject: class_name: str +class WeaviatePartialBatchError(Exception): + pass + + class Client: def __init__(self, config: Mapping[str, Any], schema: Mapping[str, str]): self.client = self.get_weaviate_client(config) @@ -32,7 +36,6 @@ def __init__(self, config: Mapping[str, Any], schema: Mapping[str, str]): self.buffered_objects: MutableMapping[str, BufferedObject] = {} def buffered_write_operation(self, stream_name: str, record: MutableMapping): - # TODO need to handle case where original DB ID is not a UUID if self.id_schema.get(stream_name, "") in record: id_field_name = self.id_schema.get(stream_name, "") record_id = generate_id(record.get(id_field_name)) @@ -47,6 +50,7 @@ def buffered_write_operation(self, stream_name: str, record: MutableMapping): del record["_id"] else: record_id = uuid.uuid4() + record_id = str(record_id) # TODO support nested objects instead of converting to json string when weaviate supports this for k, v in record.items(): @@ -79,6 +83,7 @@ def flush(self, retries: int = 3): logging.info(f"Object {result.get('id')} had errors: {errors}. Going to retry.") for object_with_error in objects_with_error: + print(self.buffered_objects) buffered_object = self.buffered_objects[object_with_error["id"]] self.client.batch.add_data_object(buffered_object.properties, buffered_object.class_name, buffered_object.id, buffered_object.vector) @@ -90,7 +95,7 @@ def flush(self, retries: int = 3): if objects_with_error and retries <= 0: error_msg = f"Objects had errors and retries failed as well: {objects_with_error}" - raise Exception(error_msg) + raise WeaviatePartialBatchError(error_msg) self.buffered_objects.clear() diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py index 8b25b593427d..93b63061383a 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py @@ -37,6 +37,11 @@ def hex_to_int(hex_str: str) -> int: def generate_id(record_id: Any) -> uuid.UUID: + try: + return uuid.UUID(record_id) + except ValueError: + pass + if isinstance(record_id, int): return uuid.UUID(int=record_id) if isinstance(record_id, str): diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/create_objects_partial_error.json b/airbyte-integrations/connectors/destination-weaviate/integration_tests/create_objects_partial_error.json new file mode 100644 index 000000000000..4521b2af2dda --- /dev/null +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/create_objects_partial_error.json @@ -0,0 +1,30 @@ +[ + { + "class": "NonExistingClass", + "creationTimeUnix": 1614852753747, + "id": "154cbccd-89f4-4b29-9c1b-001a3339d89a", + "properties": {}, + "deprecations": null, + "result": { + "errors": { + "error": [ + { + "message": "class 'NonExistingClass' not present in schema, class NonExistingClass not present" + } + ] + } + } + }, + { + "class": "ExistingClass", + "creationTimeUnix": 1614852753746, + "id": "b7b1cfbe-20da-496c-b932-008d35805f26", + "properties": {}, + "vector": [ + -0.05244319, + 0.076136276 + ], + "deprecations": null, + "result": {} + } +] \ No newline at end of file diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index 5c971ee2e940..02d993aec0aa 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -8,6 +8,7 @@ import uuid from typing import Any, Dict, List, Mapping import os +from unittest.mock import Mock import docker import pytest @@ -25,7 +26,7 @@ Type, ) from destination_weaviate import DestinationWeaviate -from destination_weaviate.client import Client +from destination_weaviate.client import Client, WeaviatePartialBatchError from destination_weaviate.utils import stream_to_class_name @@ -491,3 +492,13 @@ def test_client_delete_stream_entries(caplog, client: Client): actual_schema = client.client.schema.get("Article") title_prop = next(filter(lambda x: x["name"] == "title", actual_schema["properties"])) assert title_prop["moduleConfig"]["text2vec-contextionary"]["vectorizePropertyName"] == True, "Ensure moduleconfig is persisted" + + +def test_client_flush_partial_error(caplog, client: Client): + partial_error_result = load_json_file("create_objects_partial_error.json") + client.client.batch.create_objects = Mock(return_value=partial_error_result) + time.sleep = Mock(return_value=None) + client.buffered_write_operation("Article", {"id": "b7b1cfbe-20da-496c-b932-008d35805f26"}) + client.buffered_write_operation("Article", {"id": "154cbccd-89f4-4b29-9c1b-001a3339d89a"}) + with pytest.raises(WeaviatePartialBatchError): + client.flush() From 19541e99382c60fa78f66549daacbd07db590edc Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Mon, 9 Jan 2023 14:15:31 -0800 Subject: [PATCH 38/42] fix ID generation --- .../destination-weaviate/destination_weaviate/utils.py | 8 +++----- .../destination-weaviate/unit_tests/unit_test.py | 10 +++++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py index 93b63061383a..270f3aecbaba 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py @@ -37,18 +37,16 @@ def hex_to_int(hex_str: str) -> int: def generate_id(record_id: Any) -> uuid.UUID: - try: - return uuid.UUID(record_id) - except ValueError: - pass - if isinstance(record_id, int): return uuid.UUID(int=record_id) + if isinstance(record_id, str): id_int = hex_to_int(record_id) if hex_to_int(record_id) > 0: return uuid.UUID(int=id_int) + return uuid.UUID(record_id) + def get_schema_from_catalog(configured_catalog: ConfiguredAirbyteCatalog) -> Mapping[str, Mapping[str, str]]: schema = {} diff --git a/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py b/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py index 79eec344f90e..18b2c0e551c7 100644 --- a/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py @@ -1,10 +1,11 @@ # # Copyright (c) 2022 Airbyte, Inc., all rights reserved. # +import uuid from unittest.mock import Mock from destination_weaviate.client import Client -from destination_weaviate.utils import stream_to_class_name +from destination_weaviate.utils import stream_to_class_name, generate_id def test_client_custom_vectors_config(): @@ -35,3 +36,10 @@ def test_utils_stream_name_to_class_name(): assert stream_to_class_name("s _ a") == "S_a" assert stream_to_class_name("s{} _ a") == "S_a" assert stream_to_class_name("s{} _ aA") == "S_aA" + + +def test_generate_id(): + assert generate_id("1") == uuid.UUID(int=1) + assert generate_id("0x1") == uuid.UUID(int=1) + assert generate_id(1) == uuid.UUID(int=1) + assert generate_id("123e4567-e89b-12d3-a456-426614174000") == uuid.UUID("123e4567-e89b-12d3-a456-426614174000") From 1befb544d26d11c7adc483a660fad2f0cac13945 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Mon, 9 Jan 2023 16:24:08 -0800 Subject: [PATCH 39/42] Clean up recursive retry logic --- .../destination_weaviate/client.py | 22 +++++++++---------- .../integration_tests/integration_test.py | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index 90603c101c98..412cf0abab88 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -34,6 +34,7 @@ def __init__(self, config: Mapping[str, Any], schema: Mapping[str, str]): self.vectors = parse_vectors(config.get("vectors")) self.id_schema = parse_id_schema(config.get("id_schema")) self.buffered_objects: MutableMapping[str, BufferedObject] = {} + self.objects_with_error: MutableMapping[str, BufferedObject] = {} def buffered_write_operation(self, stream_name: str, record: MutableMapping): if self.id_schema.get(stream_name, "") in record: @@ -74,29 +75,28 @@ def buffered_write_operation(self, stream_name: str, record: MutableMapping): self.flush() def flush(self, retries: int = 3): + if len(self.objects_with_error) > 0 and retries == 0: + error_msg = f"Objects had errors and retries failed as well. Object IDs: {self.objects_with_error.keys}" + raise WeaviatePartialBatchError(error_msg) + results = self.client.batch.create_objects() - objects_with_error = [] + self.objects_with_error.clear() for result in results: errors = result.get("result", {}).get("errors", []) if errors: - objects_with_error.append({"id": result.get("id"), "errors": errors}) - logging.info(f"Object {result.get('id')} had errors: {errors}. Going to retry.") + obj_id = result.get("id") + self.objects_with_error[obj_id] = self.buffered_objects.get(obj_id) + logging.info(f"Object {obj_id} had errors: {errors}. Going to retry.") - for object_with_error in objects_with_error: - print(self.buffered_objects) - buffered_object = self.buffered_objects[object_with_error["id"]] + for buffered_object in self.objects_with_error.values(): self.client.batch.add_data_object(buffered_object.properties, buffered_object.class_name, buffered_object.id, buffered_object.vector) - if objects_with_error and retries > 0: + if len(self.objects_with_error) > 0 and retries > 0: logging.info("sleeping 2 seconds before retrying batch again") time.sleep(2) self.flush(retries - 1) - if objects_with_error and retries <= 0: - error_msg = f"Objects had errors and retries failed as well: {objects_with_error}" - raise WeaviatePartialBatchError(error_msg) - self.buffered_objects.clear() def delete_stream_entries(self, stream_name: str): diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index 02d993aec0aa..62863fd8b586 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -494,7 +494,7 @@ def test_client_delete_stream_entries(caplog, client: Client): assert title_prop["moduleConfig"]["text2vec-contextionary"]["vectorizePropertyName"] == True, "Ensure moduleconfig is persisted" -def test_client_flush_partial_error(caplog, client: Client): +def test_client_flush_partial_error(client: Client): partial_error_result = load_json_file("create_objects_partial_error.json") client.client.batch.create_objects = Mock(return_value=partial_error_result) time.sleep = Mock(return_value=None) From d577f24476adf98d695cc487b255fee5db1a9a51 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Wed, 11 Jan 2023 14:28:51 -0800 Subject: [PATCH 40/42] fix flake tests --- .../integration_tests/integration_test.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index 62863fd8b586..deea575f8c5b 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -204,8 +204,6 @@ def test_write(config: Mapping, article_catalog: ConfiguredAirbyteCatalog, clien assert expected_records == records_in_destination, "Records in destination should match records expected" - - def test_write_large_batch(config: Mapping, article_catalog: ConfiguredAirbyteCatalog, client: Client): append_stream = article_catalog.streams[0].stream.name first_state_message = _state({"state": "1"}) @@ -491,7 +489,7 @@ def test_client_delete_stream_entries(caplog, client: Client): assert count_objects(client, "Article") == 0, "Ensure articles have been deleted however class was recreated" actual_schema = client.client.schema.get("Article") title_prop = next(filter(lambda x: x["name"] == "title", actual_schema["properties"])) - assert title_prop["moduleConfig"]["text2vec-contextionary"]["vectorizePropertyName"] == True, "Ensure moduleconfig is persisted" + assert title_prop["moduleConfig"]["text2vec-contextionary"]["vectorizePropertyName"] is True, "Ensure moduleconfig is persisted" def test_client_flush_partial_error(client: Client): From cce55e62bcc27f754d434454ce49629a44fed0c5 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Wed, 11 Jan 2023 14:38:18 -0800 Subject: [PATCH 41/42] ran flake reformat --- .../destination_weaviate/client.py | 14 ++++++++------ .../destination_weaviate/destination.py | 5 +++-- .../destination_weaviate/utils.py | 12 ++++++++---- .../integration_tests/integration_test.py | 2 +- .../destination-weaviate/unit_tests/unit_test.py | 3 ++- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py index 412cf0abab88..eb831e10af80 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/client.py @@ -2,14 +2,15 @@ # Copyright (c) 2022 Airbyte, Inc., all rights reserved. # -import uuid -import logging import json -from dataclasses import dataclass +import logging import time -from typing import Any, Mapping, MutableMapping, List +import uuid +from dataclasses import dataclass +from typing import Any, List, Mapping, MutableMapping import weaviate + from .utils import generate_id, parse_id_schema, parse_vectors, stream_to_class_name @@ -89,8 +90,9 @@ def flush(self, retries: int = 3): logging.info(f"Object {obj_id} had errors: {errors}. Going to retry.") for buffered_object in self.objects_with_error.values(): - self.client.batch.add_data_object(buffered_object.properties, buffered_object.class_name, buffered_object.id, - buffered_object.vector) + self.client.batch.add_data_object( + buffered_object.properties, buffered_object.class_name, buffered_object.id, buffered_object.vector + ) if len(self.objects_with_error) > 0 and retries > 0: logging.info("sleeping 2 seconds before retrying batch again") diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py index a1e8be882ad1..49ec5c5839e3 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/destination.py @@ -1,9 +1,10 @@ # # Copyright (c) 2022 Airbyte, Inc., all rights reserved. # + +import random import string from typing import Any, Iterable, Mapping -import random from airbyte_cdk import AirbyteLogger from airbyte_cdk.destinations import Destination @@ -70,7 +71,7 @@ def check(self, logger: AirbyteLogger, config: Mapping[str, Any]) -> AirbyteConn if not ready: return AirbyteConnectionStatus(status=Status.FAILED, message=f"Weaviate server {config.get('url')} not ready") - class_name = ''.join(random.choices(string.ascii_uppercase, k=10)) + class_name = "".join(random.choices(string.ascii_uppercase, k=10)) client.schema.create_class({"class": class_name}) client.schema.delete_class(class_name) return AirbyteConnectionStatus(status=Status.SUCCEEDED) diff --git a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py index 270f3aecbaba..2f35223f6c9b 100644 --- a/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py +++ b/airbyte-integrations/connectors/destination-weaviate/destination_weaviate/utils.py @@ -1,6 +1,10 @@ -from typing import Mapping, Any -import uuid +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# + import re +import uuid +from typing import Any, Mapping from airbyte_cdk.models import ConfiguredAirbyteCatalog @@ -55,8 +59,8 @@ def get_schema_from_catalog(configured_catalog: ConfiguredAirbyteCatalog) -> Map for k, v in stream.stream.json_schema.get("properties").items(): stream_schema[k] = "default" if "array" in v.get("type", []) and ( - "object" in v.get("items", {}).get("type", []) or - "array" in v.get("items", {}).get("type", [])): + "object" in v.get("items", {}).get("type", []) or "array" in v.get("items", {}).get("type", []) + ): stream_schema[k] = "jsonify" if "object" in v.get("type", []): stream_schema[k] = "jsonify" diff --git a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py index deea575f8c5b..87a286c4c1be 100644 --- a/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/integration_tests/integration_test.py @@ -4,10 +4,10 @@ import json import logging +import os import time import uuid from typing import Any, Dict, List, Mapping -import os from unittest.mock import Mock import docker diff --git a/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py b/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py index 18b2c0e551c7..bf499c590e6c 100644 --- a/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py +++ b/airbyte-integrations/connectors/destination-weaviate/unit_tests/unit_test.py @@ -1,11 +1,12 @@ # # Copyright (c) 2022 Airbyte, Inc., all rights reserved. # + import uuid from unittest.mock import Mock from destination_weaviate.client import Client -from destination_weaviate.utils import stream_to_class_name, generate_id +from destination_weaviate.utils import generate_id, stream_to_class_name def test_client_custom_vectors_config(): From a10a1af23db34a9783e82f77af1ed38b47c9cf30 Mon Sep 17 00:00:00 2001 From: itaseski Date: Thu, 12 Jan 2023 00:13:02 +0100 Subject: [PATCH 42/42] add definitions --- .../seed/destination_definitions.yaml | 6 +++ .../resources/seed/destination_specs.yaml | 50 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/airbyte-config/init/src/main/resources/seed/destination_definitions.yaml b/airbyte-config/init/src/main/resources/seed/destination_definitions.yaml index de95948e3457..3a517c153d0c 100644 --- a/airbyte-config/init/src/main/resources/seed/destination_definitions.yaml +++ b/airbyte-config/init/src/main/resources/seed/destination_definitions.yaml @@ -436,3 +436,9 @@ icon: teradata.svg documentationUrl: https://docs.airbyte.io/integrations/destinations/teradata releaseStage: alpha +- name: Weaviate + destinationDefinitionId: 7b7d7a0d-954c-45a0-bcfc-39a634b97736 + dockerRepository: airbyte/destination-weaviate + dockerImageTag: 0.1.0 + documentationUrl: https://docs.airbyte.com/integrations/destinations/weaviate + releaseStage: alpha \ No newline at end of file diff --git a/airbyte-config/init/src/main/resources/seed/destination_specs.yaml b/airbyte-config/init/src/main/resources/seed/destination_specs.yaml index a1d6ebd5f3a4..52de3cb54266 100644 --- a/airbyte-config/init/src/main/resources/seed/destination_specs.yaml +++ b/airbyte-config/init/src/main/resources/seed/destination_specs.yaml @@ -7297,3 +7297,53 @@ supported_destination_sync_modes: - "overwrite" - "append" +- dockerImage: "airbyte/destination-weaviate:0.1.0" + spec: + documentationUrl: "https://docs.airbyte.com/integrations/destinations/weaviate" + connectionSpecification: + $schema: "http://json-schema.org/draft-07/schema#" + title: "Destination Weaviate" + type: "object" + required: + - "url" + additionalProperties: false + properties: + url: + type: "string" + description: "The URL to the weaviate instance" + examples: + - "http://localhost:8080" + - "https://your-instance.semi.network" + username: + type: "string" + description: "Username used with OIDC authentication" + examples: + - "xyz@weaviate.io" + password: + type: "string" + description: "Password used with OIDC authentication" + airbyte_secret: true + batch_size: + type: "integer" + description: "Batch size for writing to Weaviate" + default: 100 + vectors: + type: "string" + description: "Comma separated list of strings of `stream_name.vector_column_name`\ + \ to specify which field holds the vectors." + examples: + - "my_table.my_vector_column, another_table.vector" + - "mytable.vector" + id_schema: + type: "string" + description: "Comma separated list of strings of `stream_name.id_column_name`\ + \ to specify which field holds the ID of the record." + examples: + - "my_table.my_id_column, another_table.id" + - "users.user_id" + supportsIncremental: true + supportsNormalization: false + supportsDBT: false + supported_destination_sync_modes: + - "append" + - "overwrite"