|     IEEE Xplore Digital Library     |     IEEE Standards     |     IEEE Spectrum     |     More Sites

Verified Commit ff7aa272 authored by Emi Simpson's avatar Emi Simpson
Browse files

Stashed for later (i'm exploding my computer)

parent 643c1920
Pipeline #1050 failed with stage
in 52 seconds
......@@ -100,6 +100,8 @@ By looking at the code associated with the error, more information can be learne
* Update project `PUT /project/<project_id>`
* Delete project `DELETE /project/<project_id>`
* Create source `POST /project/<pid>/source`
* `backend: <backend>`
* `url: <url>`
* Delete source `DELETE /project/<pid>/source/<sid>`
* Get project statistics `GET /project/<pid>/stats`
* Returns `[Statistic]`
......@@ -13,7 +13,8 @@ from mystic.api.v1.errors import ApiErrorCode, BadSessionError, EndpointDNEError
from mystic.api.v1.types import Instance, Job, job_from_coordinator_job, lookup_backend, Project, project_from_database_project, User, user_from_database_user
from mystic.coordination import CoordinatorFlow
from mystic.queries import Query
from mystic.types import UncheckedPID, UserID
from mystic.sources import fold_sources, SOURCE_PROCESSORS
from mystic.types import Backend, UncheckedPID, Url, UserID
from mystic.utils import panic
bp = Blueprint('api_v1', __name__, url_prefix='/api/v1')
......@@ -304,6 +305,63 @@ def instance_info(_: Request) -> CoordinatorFlow[R[Instance]]:
for b in backends)
if details is not None])))
def add_source(r: Request, get_user: GetUser, pid: int) -> Query[CoordinatorFlow[R[bool]], NoReturn] | R[MissingFieldsError] | R[MalformedArgumentError]:
missing_fields = [f for f in ('backend', 'url') if f not in r.form]
if missing_fields:
return R(HTTPStatus.BAD_REQUEST, MissingFieldsError(
message=f'The keys {missing_fields} are required, but were not provided',
missing_fields = missing_fields))
backend = Backend(r.form['backend'])
url = Url(r.form['url'])
if len(url) == 0:
return R(HTTPStatus.BAD_REQUEST, MalformedArgumentError(
message='The URL field must not be empty',
field = 'url'))
# Identify which processor corresponds to this type of source
source_processor = SOURCE_PROCESSORS[backend]
except KeyError:
# And throw a fit if this source doesn't exist
return R(HTTPStatus.BAD_REQUEST, MalformedArgumentError(
message='The "backend" field must correspond to one of the backends listed in the /instance endpoint',
field = 'backend'))
source = source_processor(-1, None, backend, url, False)
validation_error = source.validate_source()
if validation_error:
return R(HTTPStatus.BAD_REQUEST, MalformedArgumentError(
field = 'url'))
return queries.BoundQuery(
get_user, # first we validate the user's session
transformation=lambda user: queries.MappedQuery( # if so...
# next, add the source to the project
queries.AddSourceIfOwned(UncheckedPID(pid), backend, url, user),
on_success=lambda source_id:
[f'p-{pid:x}', f's-{source_id}'],
f'Scan {source.display_name()} {source.get_simplified_form()}',
# if there was a problem adding the source, turn it into an error
on_error=lambda e: ({
# this shouldn't be possible, because we validated that the project
# existed in the last step
queries.AddSourceError.NonexistantProject: outcome.Error(
ProjectField(pid, ProjectFieldKind.Generic),
"[INTERNAL ERROR] This project does not exist"),
# the data source has already been added
queries.AddSourceError.SourceAlreadyPresent: outcome.Error(
ProjectField(pid, ProjectFieldKind.AddSource),
"This data source has already been added to this project"),
def get_bp(auth_mod: auth.AuthModule) -> Blueprint:
bp = Blueprint("api-v1", __name__, url_prefix="/api/v1/")
......@@ -384,7 +384,6 @@ class CoordinatorConnection:
class JobDict(TypedDict):
One of the objects returned by :func:`jobs_for_project`
......@@ -143,7 +143,7 @@ class QueryRequest(NamedTuple):
prefer to use substitutions using :attr:`args`
args: Tuple[Any, ...] | List[Any]
args: Tuple[Any, ...] | List[Any] | Mapping[str, Any]
Arguments to be substituted into :attr:`query`
......@@ -753,6 +753,77 @@ class AddSourceToProject(NamedTuple):
raise Exception(f"Unexpected error code for AddSourceToProject: {results}")
return Finished(results.last_row_id)
class AddSourceIfOwnedError(Enum):
Errors that can occur during :class:`AddSourceToProject`
NonexistantProject = auto()
The named project did not exist, despite validation
SourceAlreadyPresent = auto()
The source that was being added was already present in the database.
NoPermission = auto()
The named user was not an owner of this project
class AddSourceIfOwned(NamedTuple):
Adds a data source to a project IFF the named user is a manager
If this succeeds, then the ID of the new source is produced. Otherwise, one of the
:class:`AddSourceError`s will be produced. The conditions which produce these errors
are described in the documentation for :class:`AddSourceError`.
project_id: UncheckedPID
The ID of the project that the source is to added to
source_type: Backend
The type of the source, e.g. "github", "rss", or "git"
source_url: Url
The URL of the repository of the source
user: UserID
The user upon who's management of the project the operation depends
def get_query(self) -> QueryRequest:
return QueryRequest('''
INSERT INTO data_sources (
SELECT %(backend)s, %(url)s, %(pid)s
FROM owners
WHERE user_id = %(uid)s
AND project_id = %(pid)s
''', {
'backend': self.source_type,
'url': self.source_url,
'pid': self.project_id,
'uid': self.user})
def handle_results(self, results: QueryResult | SqlIntegrityError) -> Finished[int] | Error[AddSourceIfOwnedError]:
if isinstance(results, SqlIntegrityError):
if results.error_code == SqlErrorCode.NO_REFERENCED_ROW:
return Error(AddSourceIfOwnedError.NonexistantProject)
elif results.error_code == SqlErrorCode.DUP_ENTRY:
return Error(AddSourceIfOwnedError.SourceAlreadyPresent)
raise Exception(f"Unexpected error code for AddSourceToProject: {results}")
elif results.row_count == 0:
return Error(AddSourceIfOwnedError.NoPermission)
return Finished(results.last_row_id)
class AddOwnerByUIDError(Enum):
An error that can occur during :class:`AddOwner` or :class:`AddOwnerByUsername`
......@@ -47,7 +47,6 @@ def try_validate_password(hash: bytes, password: str) -> Optional[Literal[Valida
except argon2.exceptions.InvalidHash:
return ValidatePasswordError.InvalidServerState
class ValidatePassword(NamedTuple):
A :class:`Query` to check if a username and password pair are valid
from typing import Mapping, NewType, NamedTuple, Optional
Backend = NewType('Backend', str)
A `str`-like field exclusively for backends
Url = NewType('Url', str)
A `str`-like field exclusively for URLs
UserID = NewType('UserID', int)
UncheckedPID = NewType('UncheckedProjectID', int)
ProjectID = NewType('ProjectID', UncheckedPID)
SourceID = NewType('SourceID', int)
class Request(NamedTuple):
form: Mapping[str, str]
headers: Mapping[str, str]
query_params: Mapping[str, str]
session: Mapping[str, str] # deprecated, to be removed with frontend
user: Optional[UserID] # deprecated, to be removed with frontend
def build_request() -> 'Request':
from flask import request, session
from mystic.config import get_auth_module
return Request(
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment