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}")
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, )
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}"
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)
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)