Пример #1
0
 def test_returns_a_list_of_service_descriptors(self, m: Mocker):
     m.get('/service', text=RESPONSE_SERVICE_LIST)
     descriptors = piazza.get_services(pattern='^test-pattern$')
     self.assertIsInstance(descriptors, list)
     self.assertEqual(2, len(descriptors))
     self.assertIsInstance(descriptors[0], piazza.ServiceDescriptor)
     self.assertIsInstance(descriptors[1], piazza.ServiceDescriptor)
Пример #2
0
def test_get_grafana_service_url_success(
    api_url: str,
    iguazio_client: mlrun.api.utils.clients.iguazio.Client,
    requests_mock: requests_mock_package.Mocker,
):
    expected_grafana_url = (
        "https://grafana.default-tenant.app.hedingber-301-1.iguazio-cd2.com")
    grafana_service = {
        "spec": {
            "kind": "grafana"
        },
        "status": {
            "state":
            "ready",
            "urls": [
                {
                    "kind": "http",
                    "url": "https-has-precedence"
                },
                {
                    "kind": "https",
                    "url": expected_grafana_url
                },
            ],
        },
    }
    response_body = _generate_app_services_manifests_body([grafana_service])
    requests_mock.get(f"{api_url}/api/app_services_manifests",
                      json=response_body)
    grafana_url = iguazio_client.try_get_grafana_service_url("session-cookie")
    assert grafana_url == expected_grafana_url
Пример #3
0
    def test_facebook_for_guest_user_no_email(
            self, mock_requests: requests_mock.Mocker) -> None:
        """Auth request add a facebook account without the email for a guest user."""

        user_id, auth_token = self.create_guest_user(first_name='Pascal')

        mock_requests.get(
            'https://graph.facebook.com/v4.0/me?'
            'access_token=my-custom-token&fields=id%2Cfirst_name%2Cemail',
            json={'id': '12345'})
        self.json_from_response(
            self.app.post(
                '/api/user/authenticate',
                data='{"facebookAccessToken": "my-custom-token",'
                f'"userId": "{user_id}", "authToken": "{auth_token}"}}',
                content_type='application/json'))

        # OK the user was created without an email. Now add an email:
        response = self.app.post(
            '/api/user/authenticate',
            data=
            f'{{"email": "*****@*****.**", "userId": "{user_id}", "authToken": "{auth_token}"}}',
            content_type='application/json')
        auth_response = self.json_from_response(response)

        self.assertFalse(auth_response.get('isNewUser'))
        self.assertEqual(
            'Pascal',
            auth_response['authenticatedUser']['profile'].get('name'))
        self.assertTrue(auth_response['authenticatedUser'].get('hasAccount'))
        self.assertEqual('12345',
                         auth_response['authenticatedUser']['facebookId'])
        self.assertEqual(user_id, auth_response['authenticatedUser']['userId'])
Пример #4
0
def test_get_project_owner(
    api_url: str,
    iguazio_client: mlrun.api.utils.clients.iguazio.Client,
    requests_mock: requests_mock_package.Mocker,
):
    owner_username = "******"
    project = _generate_project(owner=owner_username)
    session = "1234"
    owner_access_key = "some-access-key"

    def verify_get(request, context):
        assert request.qs == {
            "include": ["owner"],
            "enrich_owner_access_key": ["true"],
        }
        context.status_code = http.HTTPStatus.OK.value
        _verify_project_request_headers(request.headers, session)
        return {
            "data": _build_project_response(
                iguazio_client, project, owner_access_key=owner_access_key
            )
        }

    # mock project response so store will update
    requests_mock.get(
        f"{api_url}/api/projects/__name__/{project.metadata.name}", json=verify_get,
    )
    project_owner = iguazio_client.get_project_owner(session, project.metadata.name,)
    assert project_owner.username == owner_username
    assert project_owner.session == owner_access_key
    def test_new_user_with_existing_email(self, mock_requests: requests_mock.Mocker) -> None:
        """Auth request with a pole-emploi token for a new user using an existing email."""

        user_id = self.authenticate_new_user(email='*****@*****.**', password='******')

        mock_requests.post(
            'https://authentification-candidat.pole-emploi.fr/connexion/oauth2/access_token?'
            'realm=/individu',
            json={'nonce': 'correct-nonce', 'token_type': 'Bearer', 'access_token': '123456'})
        mock_requests.get(
            'https://api.emploi-store.fr/partenaire/peconnect-individu/v1/userinfo',
            headers={'Authorization': 'Bearer 123456'},
            json={
                'email': '*****@*****.**',
                'family_name': 'CORPET',
                'gender': 'male',
                'given_name': 'PASCAL',
                'sub': 'pe-connect-user-id-1',
            })

        response = self.app.post(
            '/api/user/authenticate',
            data='{"peConnectCode": "correct-code", "peConnectNonce": "correct-nonce"}',
            content_type='application/json')
        auth_response = self.json_from_response(response)
        self.assertFalse(auth_response.get('isNewUser'))
        self.assertEqual(user_id, auth_response['authenticatedUser']['userId'])
Пример #6
0
 def test_throws_when_catalog_throws(self, m: rm.Mocker):
     scene = create_scene()
     m.get('/planet/activate/planetscope/test-scene-id',
           status_code=500,
           text='oh noes')
     with self.assertRaises(scenes.CatalogError):
         scenes.activate(scene, 'test-planet-api-key', 'test-user-id')
Пример #7
0
def mock_pand_get(m: Mocker, url: str, self_url: Optional[str] = None) -> None:
    self_url = self_url or url
    m.get(
        url,
        json={
            "identificatiecode": "0003100000118018",
            "oorspronkelijkBouwjaar": 1965,
            "status": "PandInGebruik",
            "_links": {
                "self": {
                    "href": self_url,
                },
            },
            "_embedded": {
                "geometrie": {
                    "type":
                    "Polygon",
                    "coordinates": [[
                        [5.3329181, 52.113041],
                        [5.3512001, 52.11283],
                        [5.3510284, 52.10234],
                        [5.3329181, 52.113041],
                    ]],
                },
            },
        },
    )
Пример #8
0
def mock_oas_get(m: Mocker) -> None:
    base_url = _get_base_url()
    oas_url = f"{base_url}schema/openapi.yaml"
    with open(SELECTIELIJST_API_SPEC, "rb") as api_spec:
        api_spec_content = api_spec.read()
        m.get(oas_url, content=api_spec_content)
        m.get(settings.REFERENTIELIJSTEN_API_SPEC, content=api_spec_content)
Пример #9
0
def test_refresh_keys_failure(requests_mock: Mocker, flask_app: Flask):
    requests_mock.get(
        "https://perdu.auth0.com/.well-known/jwks.json",
        exc=requests.exceptions.ConnectTimeout,
    )
    authenticator = Auth0Authenticator(flask_app)
    assert authenticator.keys == {}
Пример #10
0
    def test_linked_in_server_email_fails(
            self, mock_requests: requests_mock.Mocker,
            mock_logging: mock.MagicMock) -> None:
        """Auth request with LinkedIn, but email request fails."""

        mock_requests.post('https://www.linkedin.com/oauth/v2/accessToken',
                           json={'access_token': '123456'})
        mock_requests.get('https://api.linkedin.com/v2/me',
                          headers={'Authorization': 'Bearer 123456'},
                          status_code=200,
                          json={
                              'id': 'linked-in-user-id-1',
                              'localizedFirstName': 'Cyrille',
                              'localizedLastName': 'Corpet',
                          })
        mock_requests.get(
            'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))',
            headers={'Authorization': 'Bearer 123456'},
            status_code=400,
            text='Token outdated')

        response = self.app.post('/api/user/authenticate',
                                 data='{"linkedInCode": "correct-code"}',
                                 content_type='application/json')
        self.assertEqual(403, response.status_code)
        self.assertIn('Token outdated', response.get_data(as_text=True))

        mock_logging.assert_called_once()
Пример #11
0
def test_patch_project_only_labels(
    api_url: str,
    nuclio_client: mlrun.api.utils.clients.nuclio.Client,
    requests_mock: requests_mock_package.Mocker,
):
    project_name = "project-name"
    project_labels = {
        "some-label": "some-label-value",
    }
    mocked_project_body = _generate_project_body(
        project_name, labels={"label-key": "label-value"},
    )

    def verify_patch(request, context):
        # verifying the patch kept the old labels, patched the description, and added the new label
        expected_body = mocked_project_body
        expected_body["metadata"]["labels"].update(project_labels)
        assert (
            deepdiff.DeepDiff(expected_body, request.json(), ignore_order=True,) == {}
        )
        context.status_code = http.HTTPStatus.NO_CONTENT.value

    requests_mock.get(
        f"{api_url}/api/projects/{project_name}", json=mocked_project_body
    )
    requests_mock.put(f"{api_url}/api/projects", json=verify_patch)
    nuclio_client.patch_project(
        None, project_name, {"metadata": {"labels": project_labels}},
    )
Пример #12
0
    def test_new_user_with_existing_email(
            self, mock_requests: requests_mock.Mocker) -> None:
        """Auth request with a LinkedIn code for a new user using an existing email."""

        user_id = self.authenticate_new_user(email='*****@*****.**',
                                             password='******')

        mock_requests.post('https://www.linkedin.com/oauth/v2/accessToken',
                           json={'access_token': '123456'})
        mock_requests.get('https://api.linkedin.com/v2/me',
                          headers={'Authorization': 'Bearer 123456'},
                          json={
                              'id': 'linked-in-user-id-1',
                              'localizedFirstName': 'Pascal',
                              'localizedLastName': 'Corpet',
                          })
        mock_requests.get(
            'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))',
            headers={'Authorization': 'Bearer 123456'},
            json={'handle~': {
                'emailAddress': '*****@*****.**'
            }})

        response = self.app.post('/api/user/authenticate',
                                 data='{"linkedInCode": "correct-code"}',
                                 content_type='application/json')
        auth_response = self.json_from_response(response)
        self.assertFalse(auth_response.get('isNewUser'))
        self.assertEqual(user_id, auth_response['authenticatedUser']['userId'])
Пример #13
0
 def test_throws_when_descriptors_are_malformed(self, m: Mocker):
     mangled_response = json.loads(RESPONSE_SERVICE_LIST)
     (descriptor, _) = mangled_response['data']
     descriptor.pop('serviceId')
     m.get('/service', json=mangled_response)
     with self.assertRaises(piazza.InvalidResponse):
         piazza.get_services(pattern='^test-pattern$')
Пример #14
0
 def test_throws_when_name_is_missing(self, m: Mocker):
     mangled_response = json.loads(RESPONSE_SERVICE_LIST)
     mangled_response['data'][0]['resourceMetadata'].pop('name')
     mangled_response['data'][1]['resourceMetadata'].pop('name')
     m.get('/service', json=mangled_response)
     with self.assertRaises(piazza.InvalidResponse):
         piazza.get_services(pattern='^test-pattern$')
Пример #15
0
 def test_does_not_spam_activation_url_if_already_active(
         self, m: rm.Mocker):
     scene = create_scene()
     scene.status = scenes.STATUS_ACTIVE
     m.get('/planet/activate/planetscope/test-scene-id', text='')
     scenes.activate(scene, 'test-planet-api-key', 'test-user-id')
     self.assertEqual(0, len(m.request_history))
Пример #16
0
def test_should_make_three_requests(mocked_time: MagicMock,
                                    requests_mock: Mocker):
    # test scenario
    test_url = "http://test-url.com/"
    amount_of_requests = 3

    # mocks
    mocked_time.side_effect = [0.5, 0.6, 0.7, 0.8, 0.9, 1.0]  # 3x starts, ends

    requests_mock.get(test_url)
    requests_mock.get(test_url)
    requests_mock.get(test_url)

    # function invocation
    durations = fetch_remote_content_many_times(
        target_url=test_url, amount_of_requests=amount_of_requests)

    # durations assertion
    expected_durations = [(0.5, 0.6), (0.7, 0.8), (0.9, 1.0)]
    assert durations == expected_durations

    # requests history assertion
    requests_history = requests_mock.request_history

    assert requests_history[0].method == "GET"
    assert requests_history[0].url == test_url

    assert requests_history[1].method == "GET"
    assert requests_history[1].url == test_url

    assert requests_history[1].method == "GET"
    assert requests_history[1].url == test_url
Пример #17
0
 def test_returns_multispectral_url_if_activated(self, m: rm.Mocker):
     scene = create_scene()
     scene.status = scenes.STATUS_ACTIVE
     scene.geotiff_multispectral = 'test-geotiff-multispectral-url'
     m.get('/planet/activate/planetscope/test-scene-id', text='')
     url = scenes.activate(scene, 'test-planet-api-key', 'test-user-id')
     self.assertEqual('test-geotiff-multispectral-url', url)
 def setup_mock_data(self, mocker: requests_mock.Mocker) -> None:
     """Fetch tweets from ch and twitter.  Setup mocking if self.mock_enabled."""
     # add the mocker for the ch api calls
     mocker.get('https://api.crimsonhexagon.com/api/monitor/posts',
                text=_mock_ch_posts)
     # seaprately we need to add the mocker for the twitter api calls
     twitter.add_mockers(mocker)
Пример #19
0
def _create_project_and_assert(
    api_url: str,
    iguazio_client: mlrun.api.utils.clients.iguazio.Client,
    requests_mock: requests_mock_package.Mocker,
    project: mlrun.api.schemas.Project,
):
    session_cookie = "1234"
    job_id = "1d4c9d25-9c5c-4a34-b052-c1d3665fec5e"

    requests_mock.post(
        f"{api_url}/api/projects",
        json=functools.partial(_verify_creation, iguazio_client, project,
                               session_cookie, job_id),
    )
    mocker, num_of_calls_until_completion = _mock_job_progress(
        api_url, requests_mock, session_cookie, job_id)
    requests_mock.get(
        f"{api_url}/api/projects/__name__/{project.metadata.name}",
        json={"data": _build_project_response(iguazio_client, project)},
    )
    created_project, is_running_in_background = iguazio_client.create_project(
        session_cookie,
        project,
    )
    assert is_running_in_background is False
    assert mocker.call_count == num_of_calls_until_completion
    exclude = {"metadata": {"created"}, "status": {"state"}}
    assert (deepdiff.DeepDiff(
        project.dict(exclude=exclude),
        created_project.dict(exclude=exclude),
        ignore_order=True,
    ) == {})
    assert created_project.metadata.created is not None
    assert created_project.status.state == project.spec.desired_state
Пример #20
0
def test_download_jmp_decks_400(
    tmp_path: Path,
    requests_mock: Mocker,
    backup: bool,
):
    jmp_dir = tmp_path / "JMP"
    backup_dir = tmp_path / "JMP_last"
    temp_dir = tmp_path / "temp_decks"

    requests_mock.get(mtgjson_downloader.DECKFILE_URL, status_code=400)
    with pytest.raises(HTTPError):
        mtgjson_downloader.download_jmp_decks(dir=jmp_dir.as_posix(),
                                              temp_dir=tmp_path.as_posix(),
                                              backup=backup)
    assert not temp_dir.exists()
    assert not jmp_dir.exists()
    assert not backup_dir.exists()

    jmp_dir.mkdir()
    with pytest.raises(HTTPError):
        mtgjson_downloader.download_jmp_decks(dir=jmp_dir.as_posix(),
                                              temp_dir=tmp_path.as_posix(),
                                              backup=backup)
    assert not temp_dir.exists()
    assert jmp_dir.exists()
    assert not backup_dir.exists()
Пример #21
0
 def test_list(self, *, rmock: requests_mock.Mocker) -> None:
     rmock.get(
         "http://test/api/snapshots",
         text=
         '[{"Name":"stretch-security-1","CreatedAt":"2017-06-03T21:36:22.2692213Z",'
         '"Description":"Snapshot from mirror [stretch-security]: '
         'http://security.debian.org/debian-security/ stretch/updates"},'
         '{"Name":"stretch-updates-1","CreatedAt":"2017-06-03T21:36:22.431767659Z",'
         '"Description":"Snapshot from mirror [stretch-updates]: '
         'http://ftp-stud.hs-esslingen.de/debian/ stretch-updates"}]')
     self.assertSequenceEqual(self.sapi.list(), [
         Snapshot(
             name='stretch-security-1',
             description=
             'Snapshot from mirror [stretch-security]: http://security.debian.org/debian-security/ '
             'stretch/updates',
             created_at=iso8601.parse_date('2017-06-03T21:36:22.2692213Z')),
         Snapshot(
             name='stretch-updates-1',
             description=
             'Snapshot from mirror [stretch-updates]: http://ftp-stud.hs-esslingen.de/debian/ '
             'stretch-updates',
             created_at=iso8601.parse_date(
                 '2017-06-03T21:36:22.431767659Z'))
     ])
Пример #22
0
def test_mtgjson_downloader_main(
    tmp_path: Path,
    requests_mock: Mocker,
    zip_one: BytesIO,
    jmp: bool,
    jmp_backup: bool,
    temp_config: Config,
):
    mtgjson_file = tmp_path / "AllPrintings.json"
    backup_file = tmp_path / "AllPrintings_last.json"
    jmp_dir = tmp_path / "JMP"
    backup_dir = tmp_path / "JMP_last"
    temp_dir = tmp_path / "temp_decks"

    with open(mtgjson_file, "w") as f:
        json.dump({"first": True}, f)
    if jmp_backup:
        jmp_dir.mkdir()

    requests_mock.get(mtgjson_downloader.MTGJSON_URL, json={"second": True})
    requests_mock.get(mtgjson_downloader.DECKFILE_URL, body=zip_one)
    mtgjson_downloader.main(config=temp_config, jmp=jmp, jmp_backup=jmp_backup)
    assert mtgjson_file.is_file()
    assert backup_file.is_file()
    with open(mtgjson_file) as f:
        assert json.load(f)["second"]
    with open(backup_file) as f:
        assert json.load(f)["first"]
    assert not temp_dir.exists()
    assert jmp_dir.exists() == jmp
    assert backup_dir.exists() == jmp_backup
Пример #23
0
def test_list_project_with_updated_after(
    api_url: str,
    iguazio_client: mlrun.api.utils.clients.iguazio.Client,
    requests_mock: requests_mock_package.Mocker,
):
    project = _generate_project()
    session = "1234"
    updated_after = datetime.datetime.now(tz=datetime.timezone.utc)

    def verify_list(request, context):
        assert request.qs == {
            "filter[updated_at]": [
                f"[$gt]{updated_after.isoformat().split('+')[0]}Z".lower()
            ],
            # TODO: Remove me when zebo returns owner
            "include": ["owner"],
        }
        context.status_code = http.HTTPStatus.OK.value
        _verify_project_request_headers(request.headers, session)
        return {"data": [_build_project_response(iguazio_client, project)]}

    # mock project response so store will update
    requests_mock.get(
        f"{api_url}/api/projects", json=verify_list,
    )
    iguazio_client.list_projects(
        session, updated_after,
    )
Пример #24
0
def test_download_jmp_decks(
    tmp_path: Path,
    requests_mock: Mocker,
    zip_one: BytesIO,
    zip_two: BytesIO,
    backup: bool,
):
    jmp_dir = tmp_path / "JMP"
    backup_dir = tmp_path / "JMP_last"
    temp_dir = tmp_path / "temp_decks"

    requests_mock.get(mtgjson_downloader.DECKFILE_URL, body=zip_one)
    mtgjson_downloader.download_jmp_decks(dir=jmp_dir.as_posix(),
                                          temp_dir=tmp_path.as_posix(),
                                          backup=False)
    assert not temp_dir.exists()
    assert (jmp_dir / "decks" / "deck1_JMP.json").is_file()
    assert not backup_dir.exists()

    requests_mock.get(mtgjson_downloader.DECKFILE_URL, body=zip_two)
    mtgjson_downloader.download_jmp_decks(dir=jmp_dir.as_posix(),
                                          temp_dir=tmp_path.as_posix(),
                                          backup=backup)
    assert not temp_dir.exists()
    assert (jmp_dir / "decks" / "deck2_JMP.json").is_file()
    assert (backup_dir / "decks" / "deck1_JMP.json").is_file() == backup
    with open(jmp_dir / "decks" / "deck2_JMP.json") as f:
        assert json.load(f)["deck2"]
    if backup:
        with open(backup_dir / "decks" / "deck1_JMP.json") as f:
            assert json.load(f)["deck1"]
Пример #25
0
    def test_generate_cube_failure(self, m: requests_mock.Mocker):
        m.post(f'{self.ENDPOINT_URL}oauth/token',
               json={
                   "access_token": "4ccsstkn983456jkfde",
                   "token_type": "bearer"
               })

        m.put(f'{self.ENDPOINT_URL}cubegens',
              response_list=[
                  make_result(0, 4),
              ])

        m.get(f'{self.ENDPOINT_URL}cubegens/93',
              response_list=[
                  make_result(1, 4),
                  make_result(2, 4,
                              failed=True,
                              output=['1.that', '2.was', '3.bad']),
              ])

        observer = TestProgressObserver()
        with new_progress_observers(observer):
            cube_result = self.generator.generate_cube(self.REQUEST)
            self.assertEqual('error', cube_result.status)
            self.assertEqual(['1.that', '2.was', '3.bad'], cube_result.output)

        print(observer.calls)
        self.assertEqual(
            [
                ('begin', [('Generating cube', 0.0, False)]),
                ('update', [('Generating cube', 0.25, False)]),
                ('end', [('Generating cube', 0.25, True)])
            ],
            observer.calls)
Пример #26
0
 def test_throws_when_scene_does_not_exist(self, m: rm.Mocker):
     scene = create_scene()
     m.get('/planet/activate/planetscope/test-scene-id',
           status_code=404,
           text='wat')
     with self.assertRaises(scenes.NotFound):
         scenes.activate(scene, 'test-planet-api-key', 'test-user-id')
Пример #27
0
    def test_facebook_with_existing_email_for_guest_user(
            self, mock_requests: requests_mock.Mocker) -> None:
        """Auth request to add a facebook account for a guest user with a pre-existing email."""

        self.authenticate_new_user(email='*****@*****.**',
                                   password='******')

        user_id, auth_token = self.create_guest_user(first_name='Pascal')

        mock_requests.get(
            'https://graph.facebook.com/v4.0/me?'
            'access_token=my-custom-token&fields=id%2Cfirst_name%2Cemail',
            json={
                'id': '12345',
                'first_name': 'Pascal',
                'email': '*****@*****.**',
            })
        response = self.app.post(
            '/api/user/authenticate',
            data='{"facebookAccessToken": "my-custom-token",'
            f'"userId": "{user_id}", "authToken": "{auth_token}"}}',
            content_type='application/json')
        auth_response = self.json_from_response(response)
        self.assertFalse(auth_response.get('isNewUser'))
        self.assertEqual('12345',
                         auth_response['authenticatedUser']['facebookId'])
Пример #28
0
 def test_throws_when_not_permitted(self, m: rm.Mocker):
     scene = create_scene()
     m.get('/planet/activate/planetscope/test-scene-id',
           status_code=401,
           text='oopsie')
     with self.assertRaises(scenes.NotPermitted):
         scenes.activate(scene, 'test-planet-api-key', 'test-user-id')
Пример #29
0
    def test_new_user(self, mock_requests: requests_mock.Mocker) -> None:
        """Auth request with a facebook token for a new user."""

        mock_requests.get(
            'https://graph.facebook.com/v4.0/me?'
            'access_token=my-custom-token&fields=id%2Cfirst_name%2Cemail',
            json={
                'id': '12345',
                'first_name': 'Pascal',
                'email': '*****@*****.**',
            })
        response = self.app.post(
            '/api/user/authenticate',
            data=json.dumps({'facebookAccessToken': 'my-custom-token'}),
            content_type='application/json')
        auth_response = self.json_from_response(response)

        self.assertTrue(auth_response['isNewUser'])
        self.assertEqual('12345',
                         auth_response['authenticatedUser']['facebookId'])
        self.assertEqual(
            'Pascal', auth_response['authenticatedUser'].get('profile',
                                                             {}).get('name'))
        self.assertEqual(
            '*****@*****.**',
            auth_response['authenticatedUser'].get('profile', {}).get('email'))
        self.assertTrue(auth_response['authenticatedUser'].get('hasAccount'))
        user_id = auth_response['authenticatedUser']['userId']
        self.assertEqual([user_id],
                         [str(u['_id']) for u in self._user_db.user.find()])
Пример #30
0
 def test_throws_when_result_is_missing(self, m: Mocker):
     mangled_response = json.loads(RESPONSE_JOB_SUCCESS)
     mangled_response['data'].pop('result')
     m.get('/job/test-job-id', json=mangled_response)
     with self.assertRaisesRegex(piazza.InvalidResponse,
                                 'missing `data.result`'):
         piazza.get_status('test-job-id')