def test_create_job(timer_client, start, interval):
    meta = load_response(timer_client.create_job).metadata
    transfer_client = TransferClient()
    transfer_client.get_submission_id = lambda *_0, **_1: {"value": "mock"}
    transfer_data = TransferData(transfer_client, GO_EP1_ID, GO_EP2_ID)
    timer_job = TimerJob.from_transfer_data(transfer_data, start, interval)
    response = timer_client.create_job(timer_job)
    assert response.http_status == 201
    assert response.data["job_id"] == meta["job_id"]
    timer_job = TimerJob.from_transfer_data(dict(transfer_data), start,
                                            interval)
    response = timer_client.create_job(timer_job)
    assert response.http_status == 201
    assert response.data["job_id"] == meta["job_id"]
    req_body = json.loads(get_last_request().body)
    if isinstance(start, datetime):
        assert req_body["start"] == start.isoformat()
    else:
        assert req_body["start"] == start
    if isinstance(interval, timedelta):
        assert req_body["interval"] == interval.total_seconds()
    else:
        assert req_body["interval"] == interval
    assert req_body["callback_url"] == slash_join(get_service_url("actions"),
                                                  "/transfer/transfer/run")
def test_slash_join(a, b):
    """
    slash_joins a's with and without trailing "/"
    to b's with and without leading "/"
    Confirms all have the same correct slash_join output
    """
    assert utils.slash_join(a, b) == "a/b"
    def __init__(
        self,
        auth_client: "globus_sdk.AuthClient",
        requested_scopes: Optional[scopes._ScopeCollectionType] = None,
        redirect_uri: Optional[str] = None,
        state: str = "_default",
        verifier: Optional[str] = None,
        refresh_tokens: bool = False,
        prefill_named_grant: Optional[str] = None,
    ):
        self.auth_client = auth_client

        # set client_id, then check for validity
        self.client_id = auth_client.client_id
        if not self.client_id:
            logger.error(
                "Invalid auth_client ID to start Native App Flow: {}".format(
                    self.client_id
                )
            )
            raise GlobusSDKUsageError(
                f'Invalid value for client_id. Got "{self.client_id}"'
            )

        # convert scopes iterable to string immediately on load
        # and default to the default requested scopes
        self.requested_scopes = scopes.MutableScope.scopes2str(
            requested_scopes or DEFAULT_REQUESTED_SCOPES
        )

        # default to `/v2/web/auth-code` on whatever environment we're looking
        # at -- most typically it will be `https://auth.globus.org/`
        self.redirect_uri = redirect_uri or (
            utils.slash_join(auth_client.base_url, "/v2/web/auth-code")
        )

        # make a challenge and secret to keep
        # if the verifier is provided, it will just be passed back to us, and
        # if not, one will be generated
        self.verifier, self.challenge = make_native_app_challenge(verifier)

        # store the remaining parameters directly, with no transformation
        self.refresh_tokens = refresh_tokens
        self.state = state
        self.prefill_named_grant = prefill_named_grant

        logger.debug("Starting Native App Flow with params:")
        logger.debug(f"auth_client.client_id={auth_client.client_id}")
        logger.debug(f"redirect_uri={self.redirect_uri}")
        logger.debug(f"refresh_tokens={refresh_tokens}")
        logger.debug(f"state={state}")
        logger.debug(f"requested_scopes={self.requested_scopes}")
        logger.debug(f"verifier=<REDACTED>,challenge={self.challenge}")

        if prefill_named_grant is not None:
            logger.debug(f"prefill_named_grant={self.prefill_named_grant}")
Exemple #4
0
    def from_transfer_data(
        cls,
        transfer_data: Union[TransferData, Dict[str, Any]],
        start: Union[datetime.datetime, str],
        interval: Union[datetime.timedelta, int, None],
        *,
        name: Optional[str] = None,
        stop_after: Optional[datetime.datetime] = None,
        stop_after_n: Optional[int] = None,
        scope: Optional[str] = None,
        environment: Optional[str] = None,
    ) -> "TimerJob":
        r"""
        Specify data to create a Timer job using the parameters for a transfer. Timer
        will use those parameters to run the defined transfer operation, recurring at
        the given interval.

        :param transfer_data: A :class:`TransferData <globus_sdk.TransferData>` object.
            Construct this object exactly as you would normally; Timer will use this to
            run the recurring transfer.
        :type transfer_data: globus_sdk.TransferData
        :param start: The datetime at which to start the Timer job.
        :type start: datetime.datetime or str
        :param interval: The interval at which the Timer job should recur. Interpreted
            as seconds if specified as an integer. If ``stop_after_n == 1``, i.e. the
            job is set to run only a single time, then interval *must* be None.
        :type interval: datetime.timedelta or int
        :param name: A (not necessarily unique) name to identify this job in Timer
        :type name: str, optional
        :param stop_after: A date after which the Timer job will stop running
        :type stop_after: datetime.datetime, optional
        :param stop_after_n: A number of executions after which the Timer job will stop
        :type stop_after_n: int
        :param scope: Timer defaults to the Transfer 'all' scope. Use this parameter to
            change the scope used by Timer when calling the Transfer Action Provider.
        :type scope: str, optional
        :param environment: For internal use: because this method needs to generate a
            URL for the Transfer Action Provider, this argument can control which
            environment the Timer job is sent to.
        :type environment: str, optional
        """
        transfer_action_url = slash_join(
            get_service_url("actions", environment=environment),
            "transfer/transfer/run")
        # dict will either convert a `TransferData` object or leave us with a dict here
        callback_body = {"body": dict(transfer_data)}
        return cls(
            transfer_action_url,
            callback_body,
            start,
            interval,
            name=name,
            stop_after=stop_after,
            stop_after_n=stop_after_n,
            scope=scope,
        )
Exemple #5
0
    def __init__(
        self,
        *,
        environment: Optional[str] = None,
        base_url: Optional[str] = None,
        authorizer: Optional[GlobusAuthorizer] = None,
        app_name: Optional[str] = None,
        transport_params: Optional[Dict[str, Any]] = None,
    ):
        # explicitly check the `service_name` to ensure that it was set
        #
        # unfortunately, we can't rely on declaring BaseClient as an ABC because it
        # doesn't have any abstract methods
        #
        # if we declare `service_name` without a value, we get AttributeError on access
        # instead of the (desired) TypeError when instantiating a BaseClient because
        # it's abstract
        if self.service_name == "_base":
            raise NotImplementedError(
                "Cannot instantiate clients which do not set a 'service_name'")
        log.info(
            f'Creating client of type {type(self)} for service "{self.service_name}"'
        )

        # if an environment was passed, it will be used, but otherwise lookup
        # the env var -- and in the special case of `production` translate to
        # `default`, regardless of the source of that value
        # logs the environment when it isn't `default`
        self.environment = config.get_environment_name(environment)

        self.transport = self.transport_class(**(transport_params or {}))
        log.debug(f"initialized transport of type {type(self.transport)}")

        self.base_url = utils.slash_join(
            config.get_service_url(self.service_name,
                                   environment=self.environment)
            if base_url is None else base_url,
            self.base_path,
        )

        self.authorizer = authorizer

        # set application name if given
        self._app_name = None
        if app_name is not None:
            self.app_name = app_name

        # setup paginated methods
        self.paginated = PaginatorTable(self)
    def get_authorize_url(self, query_params: Optional[Dict[str, Any]] = None) -> str:
        """
        Start a Native App flow by getting the authorization URL to which users
        should be sent.

        :param query_params: Additional query parameters to include in the
            authorize URL. Primarily for internal use
        :type query_params: dict, optional
        :rtype: ``string``

        The returned URL string is encoded to be suitable to display to users
        in a link or to copy into their browser. Users will be redirected
        either to your provided ``redirect_uri`` or to the default location,
        with the ``auth_code`` embedded in a query parameter.
        """
        authorize_base_url = utils.slash_join(
            self.auth_client.base_url, "/v2/oauth2/authorize"
        )
        logger.debug(f"Building authorization URI. Base URL: {authorize_base_url}")
        logger.debug(f"query_params={query_params}")

        params = {
            "client_id": self.client_id,
            "redirect_uri": self.redirect_uri,
            "scope": self.requested_scopes,
            "state": self.state,
            "response_type": "code",
            "code_challenge": self.challenge,
            "code_challenge_method": "S256",
            "access_type": (self.refresh_tokens and "offline") or "online",
        }
        if self.prefill_named_grant is not None:
            params["prefill_named_grant"] = self.prefill_named_grant
        if query_params:
            params.update(query_params)

        encoded_params = urllib.parse.urlencode(params)
        return f"{authorize_base_url}?{encoded_params}"
Exemple #7
0
def register_api_route(service,
                       path,
                       method=responses.GET,
                       adding_headers=None,
                       replace=False,
                       **kwargs):
    """
    Handy wrapper for adding URIs to the response mock state.
    """
    base_url_map = {
        "auth": "https://auth.globus.org/",
        "nexus": "https://nexus.api.globusonline.org/",
        "groups": "https://groups.api.globus.org/",
        "transfer": "https://transfer.api.globus.org/v0.10",
        "search": "https://search.api.globus.org/",
        "gcs": "https://abc.xyz.data.globus.org/api/",
    }
    assert service in base_url_map
    base_url = base_url_map.get(service)
    full_url = utils.slash_join(base_url, path)

    # can set it to `{}` explicitly to clear the default
    if adding_headers is None:
        adding_headers = {"Content-Type": "application/json"}

    if replace:
        responses.replace(method,
                          full_url,
                          headers=adding_headers,
                          match_querystring=None,
                          **kwargs)
    else:
        responses.add(method,
                      full_url,
                      headers=adding_headers,
                      match_querystring=None,
                      **kwargs)
Exemple #8
0
    def request(
        self,
        method: str,
        path: str,
        *,
        query_params: Optional[Dict[str, Any]] = None,
        data: DataParamType = None,
        headers: Optional[Dict[str, str]] = None,
        encoding: Optional[str] = None,
        allow_redirects: bool = True,
        stream: bool = False,
    ) -> GlobusHTTPResponse:
        """
        Send an HTTP request

        :param method: HTTP request method, as an all caps string
        :type method: str
        :param path: Path for the request, with or without leading slash
        :type path: str
        :param query_params: Parameters to be encoded as a query string
        :type query_params: dict, optional
        :param headers: HTTP headers to add to the request
        :type headers: dict
        :param data: Data to send as the request body. May pass through encoding.
        :type data: dict or str
        :param encoding: A way to encode request data. "json", "form", and "text"
            are all valid values. Custom encodings can be used only if they are
            registered with the transport. By default, strings get "text" behavior and
            all other objects get "json".
        :type encoding: str
        :param allow_redirects: Follow Location headers on redirect response
            automatically. Defaults to ``True``
        :type allow_redirects: bool
        :param stream: Do not immediately download the response content. Defaults to
            ``False``
        :type stream: bool

        :return: :class:`GlobusHTTPResponse \
        <globus_sdk.response.GlobusHTTPResponse>` object

        :raises GlobusAPIError: a `GlobusAPIError` will be raised if the response to the
            request is received and has a status code in the 4xx or 5xx categories
        """
        # prepare data...
        # copy headers if present
        rheaders = {**headers} if headers else {}

        # if a client is asked to make a request against a full URL, not just the path
        # component, then do not resolve the path, simply pass it through as the URL
        if path.startswith("https://") or path.startswith("http://"):
            url = path
        else:
            url = utils.slash_join(self.base_url, urllib.parse.quote(path))

        # make the request
        log.debug("request will hit URL: %s", url)
        r = self.transport.request(
            method=method,
            url=url,
            data=data.data if isinstance(data, utils.PayloadWrapper) else data,
            query_params=query_params,
            headers=rheaders,
            encoding=encoding,
            authorizer=self.authorizer,
            allow_redirects=allow_redirects,
            stream=stream,
        )
        log.debug("request made to URL: %s", r.url)

        if 200 <= r.status_code < 400:
            log.debug(f"request completed with response code: {r.status_code}")
            return GlobusHTTPResponse(r, self)

        log.debug(
            f"request completed with (error) response code: {r.status_code}")
        raise self.error_class(r)