from typing import List, Dict, Optional, Any from django.contrib.auth import get_user_model from django.http import HttpResponse from ninja import Router, Schema, Field, Form from utilities.chem import FormatConverter from .logic import PackageManager from .models import Compound, CompoundStructure, Package, User, UserPackagePermission, Rule, Reaction, Scenario, Pathway def _anonymous_or_real(request): if request.user.is_authenticated and not request.user.is_anonymous: return request.user return get_user_model().objects.get(username='anonymous') # router = Router(auth=SessionAuth()) router = Router() class Error(Schema): message: str class SimpleObject(Schema): id: str = Field(None, alias="url") name: str = Field(None, alias="name") reviewStatus: bool = Field(None, alias="package.reviewed") ################ # Login/Logout # ################ class SimpleUser(Schema): id: str = Field(None, alias="url") identifier: str = 'user' name: str = Field(None, alias='username') email: str = Field(None, alias='email') @router.post("/", response={200: SimpleUser, 403: Error}) def login(request, loginusername: Form[str], loginpassword: Form[str], hiddenMethod: Form[str]): from django.contrib.auth import authenticate from django.contrib.auth import login email = User.objects.get(username=loginusername).email user = authenticate(username=email, password=loginpassword) if user: login(request, user) return user else: return 403, {'message': 'Invalid username or password'} class SimpleGroup(Schema): id: str identifier: str = 'group' name: str ########### # Package # ########### class SimplePackage(SimpleObject): identifier: str = 'package' reviewStatus: bool = Field(None, alias="reviewed") class PackageSchema(Schema): description: str = Field(None, alias="description") id: str = Field(None, alias="url") links: List[Dict[str, List[str | int]]] = Field([], alias="links") name: str = Field(None, alias="name") primaryGroup: Optional[SimpleGroup] = None readers: List[Dict[str, str]] = Field([], alias="readers") reviewComment: str = Field(None, alias="review_comment") reviewStatus: str = Field(None, alias="review_status") writers: List[Dict[str, str]] = Field([], alias="writers") @staticmethod def resolve_links(obj: Package): return [ { 'Pathways': [ f'{obj.url}/pathway', obj.pathways.count() ] }, { 'Rules': [ f'{obj.url}/rule', obj.rules.count() ] }, { 'Compounds': [ f'{obj.url}/compound', obj.compounds.count() ] }, { 'Reactions': [ f'{obj.url}/reaction', obj.reactions.count() ] }, { 'Relative Reasoning': [ f'{obj.url}/relative-reasoning', obj.models.count() ] }, { 'Scenarios': [ f'{obj.url}/scenario', obj.scenarios.count() ] } ] @staticmethod def resolve_readers(obj: Package): users = User.objects.filter( id__in=UserPackagePermission.objects.filter( package=obj, permission=UserPackagePermission.READ[0] ).values_list('user', flat=True) ).distinct() return [{u.id: u.name} for u in users] @staticmethod def resolve_writers(obj: Package): users = User.objects.filter( id__in=UserPackagePermission.objects.filter( package=obj, permission=UserPackagePermission.WRITE[0] ).values_list('user', flat=True) ).distinct() return [{u.id: u.name} for u in users] @staticmethod def resolve_review_comment(obj): return "" @staticmethod def resolve_review_status(obj): return 'reviewed' if obj.reviewed else 'unreviewed' class PackageWrapper(Schema): package: List['PackageSchema'] @router.get("/package", response={200: PackageWrapper, 403: Error}) def get_packages(request): return {'package': PackageManager.get_all_readable_packages(request.user, include_reviewed=True)} @router.get("/package/{uuid:package_uuid}", response={200: PackageSchema, 403: Error}) def get_package(request, package_uuid): try: return PackageManager.get_package_by_id(request.user, package_uuid) except ValueError: return 403, { 'message': f'Getting Package with id {package_uuid} failed due to insufficient rights!'} ################################ # Compound / CompoundStructure # ################################ class SimpleCompound(SimpleObject): identifier: str = 'compound' class CompoundPathwayScenario(Schema): scenarioId: str scenarioName: str scenarioType: str class CompoundSchema(Schema): aliases: List[str] = Field([], alias="aliases") description: str = Field(None, alias="description") externalReferences: Dict[str, List[str]] = Field(None, alias="external_references") id: str = Field(None, alias="url") halflifes: List[Dict[str, str]] = Field([], alias="halflifes") identifier: str = 'compound' imageSize: int = 600 name: str = Field(None, alias="name") pathwayScenarios: List[CompoundPathwayScenario] = Field([], alias="pathway_scenarios") pathways: List['SimplePathway'] = Field([], alias="related_pathways") pubchemCompoundReferences: List[str] = Field([], alias="pubchem_compound_references") reactions: List['SimpleReaction'] = Field([], alias="related_reactions") reviewStatus: str = Field(False, alias="review_status") scenarios: List['SimpleScenario'] = Field([], alias="scenarios") structures: List['CompoundStructureSchema'] = [] @staticmethod def resolve_review_status(obj: CompoundStructure): return 'reviewed' if obj.package.reviewed else 'unreviewed' @staticmethod def resolve_external_references(obj: Compound): # TODO return {} @staticmethod def resolve_structures(obj: Compound): return CompoundStructure.objects.filter(compound=obj) @staticmethod def resolve_halflifes(obj: Compound): return [] @staticmethod def resolve_pubchem_compound_references(obj: Compound): return [] @staticmethod def resolve_pathway_scenarios(obj: Compound): return [ { 'scenarioId': 'https://envipath.org/package/5882df9c-dae1-4d80-a40e-db4724271456/scenario/cd8350cd-4249-4111-ba9f-4e2209338501', 'scenarioName': 'Fritz, R. & Brauner, A. (1989) - (00004)', 'scenarioType': 'Soil' } ] class CompoundWrapper(Schema): compound: List['SimpleCompound'] class SimpleCompoundStructure(SimpleObject): identifier: str = 'structure' reviewStatus: bool = Field(None, alias="compound.package.reviewed") class CompoundStructureSchema(Schema): InChI: str = Field(None, alias="inchi") aliases: List[str] = Field([], alias="aliases") canonicalSmiles: str = Field(None, alias="canonical_smiles") charge: int = Field(None, alias="charge") description: str = Field(None, alias="description") externalReferences: Dict[str, List[str]] = Field(None, alias="external_references") formula: str = Field(None, alias="formula") halflifes: List[Dict[str, str]] = Field([], alias="halflifes") id: str = Field(None, alias="url") identifier: str = 'structure' imageSize: int = 600 inchikey: str = Field(None, alias="inchikey") isDefaultStructure: bool = Field(None, alias="is_default_structure") mass: float = Field(None, alias="mass") name: str = Field(None, alias="name") pathways: List['SimplePathway'] = Field([], alias="related_pathways") pubchemCompoundReferences: List[str] = Field([], alias="pubchem_compound_references") reactions: List['SimpleReaction'] = Field([], alias="related_reactions") reviewStatus: str = Field(None, alias="review_status") scenarios: List['SimpleScenario'] = Field([], alias="scenarios") smiles: str = Field(None, alias="smiles") @staticmethod def resolve_review_status(obj: CompoundStructure): return 'reviewed' if obj.compound.package.reviewed else 'unreviewed' @staticmethod def resolve_inchi(obj: CompoundStructure): return FormatConverter.InChI(obj.smiles) @staticmethod def resolve_charge(obj: CompoundStructure): print(obj.smiles) print(FormatConverter.charge(obj.smiles)) return FormatConverter.charge(obj.smiles) @staticmethod def resolve_formula(obj: CompoundStructure): return FormatConverter.formula(obj.smiles) @staticmethod def resolve_mass(obj: CompoundStructure): return FormatConverter.mass(obj.smiles) @staticmethod def resolve_external_references(obj: CompoundStructure): # TODO return {} @staticmethod def resolve_halflifes(obj: CompoundStructure): return [] @staticmethod def resolve_pubchem_compound_references(obj: CompoundStructure): return [] @staticmethod def resolve_pathway_scenarios(obj: CompoundStructure): return [ { 'scenarioId': 'https://envipath.org/package/5882df9c-dae1-4d80-a40e-db4724271456/scenario/cd8350cd-4249-4111-ba9f-4e2209338501', 'scenarioName': 'Fritz, R. & Brauner, A. (1989) - (00004)', 'scenarioType': 'Soil' } ] class CompoundStructureWrapper(Schema): structure: List['SimpleCompoundStructure'] @router.get("/compound", response={200: CompoundWrapper, 403: Error}) def get_compounds(request): qs = Compound.objects.none() for p in PackageManager.get_reviewed_packages(): qs |= Compound.objects.filter(package=p) return {'compound': qs} @router.get("/package/{uuid:package_uuid}/compound", response={200: CompoundWrapper, 403: Error}) def get_package_compounds(request, package_uuid): try: p = PackageManager.get_package_by_id(request.user, package_uuid) return {'compound': Compound.objects.filter(package=p).prefetch_related('package')} except ValueError: return 403, { 'message': f'Getting Compounds for Package with id {package_uuid} failed due to insufficient rights!'} @router.get("/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}", response={200: CompoundSchema, 403: Error}) def get_package_compound(request, package_uuid, compound_uuid): try: p = PackageManager.get_package_by_id(request.user, package_uuid) return Compound.objects.get(package=p, uuid=compound_uuid) except ValueError: return 403, { 'message': f'Getting Compound with id {compound_uuid} failed due to insufficient rights!'} @router.get("/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure", response={200: CompoundStructureWrapper, 403: Error}) def get_package_compound_structures(request, package_uuid, compound_uuid): try: p = PackageManager.get_package_by_id(request.user, package_uuid) return {'structure': Compound.objects.get(package=p, uuid=compound_uuid).structures.all()} except ValueError: return 403, { 'message': f'Getting CompoundStructures for Compound with id {compound_uuid} failed due to insufficient rights!'} @router.get("/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/{uuid:structure_uuid}", response={200: CompoundStructureSchema, 403: Error}) def get_package_compound_structure(request, package_uuid, compound_uuid, structure_uuid): try: p = PackageManager.get_package_by_id(request.user, package_uuid) return CompoundStructure.objects.get(uuid=structure_uuid, compound=Compound.objects.get(package=p, uuid=compound_uuid)) except ValueError: return 403, { 'message': f'Getting CompoundStructure with id {structure_uuid} failed due to insufficient rights!'} ######### # Rules # ######### class SimpleRule(SimpleObject): identifier: str = 'rule' @staticmethod def resolve_url(obj: Rule): return obj.url.replace('-ambit-', '-').replace('-rdkit-', '-') class RuleWrapper(Schema): rule: List['SimpleRule'] class SimpleRuleSchema(Schema): aliases: List[str] = Field([], alias="aliases") description: str = Field(None, alias="description") ecNumbers: List[Dict[str, str]] = Field([], alias="ec_numbers") engine: str = 'ambit' id: str = Field(None, alias="url") identifier: str = Field(None, alias="identifier") isCompositeRule: bool = False name: str = Field(None, alias="name") pathways: List['SimplePathway'] = Field([], alias="related_pathways") productFilterSmarts: str = Field("", alias="product_filter_smarts") productSmarts: str = Field(None, alias="products_smarts") reactantFilterSmarts: str = Field("", alias="reactant_filter_smarts") reactantSmarts: str = Field(None, alias="reactants_smarts") reactions: List['SimpleReaction'] = Field([], alias="related_reactions") reviewStatus: str = Field(None, alias="review_status") scenarios: List['SimpleScenario'] = Field([], alias="scenarios") smirks: str = Field("", alias="smirks") # TODO transformations: str = Field("", alias="transformations") @staticmethod def resolve_url(obj: Rule): return obj.url.replace('-ambit-', '-').replace('-rdkit-', '-') @staticmethod def resolve_identifier(obj: Rule): if 'simple-rule' in obj.url: return 'simple-rule' if 'simple-ambit-rule' in obj.url: return 'simple-rule' elif 'parallel-rule' in obj.url: return 'parallel-rule' elif 'sequential-rule' in obj.url: return 'sequential-rule' else: return None @staticmethod def resolve_review_status(obj: Rule): return 'reviewed' if obj.package.reviewed else 'unreviewed' @staticmethod def resolve_product_filter_smarts(obj: Rule): return obj.product_filter_smarts if obj.product_filter_smarts else '' @staticmethod def resolve_reactant_filter_smarts(obj: Rule): return obj.reactant_filter_smarts if obj.reactant_filter_smarts else '' class CompositeRuleSchema(Schema): aliases: List[str] = Field([], alias="aliases") description: str = Field(None, alias="description") ecNumbers: List[Dict[str, str]] = Field([], alias="ec_numbers") id: str = Field(None, alias="url") identifier: str = Field(None, alias="identifier") isCompositeRule: bool = True name: str = Field(None, alias="name") pathways: List['SimplePathway'] = Field([], alias="related_pathways") productFilterSmarts: str = Field("", alias="product_filter_smarts") reactantFilterSmarts: str = Field("", alias="reactant_filter_smarts") reactions: List['SimpleReaction'] = Field([], alias="related_reactions") reviewStatus: str = Field(None, alias="review_status") scenarios: List['SimpleScenario'] = Field([], alias="scenarios") simpleRules: List['SimpleRule'] = Field([], alias="simple_rules") @staticmethod def resolve_ec_numbers(obj: Rule): return [] @staticmethod def resolve_url(obj: Rule): return obj.url.replace('-ambit-', '-').replace('-rdkit-', '-') @staticmethod def resolve_identifier(obj: Rule): if 'simple-rule' in obj.url: return 'simple-rule' if 'simple-ambit-rule' in obj.url: return 'simple-rule' elif 'parallel-rule' in obj.url: return 'parallel-rule' elif 'sequential-rule' in obj.url: return 'sequential-rule' else: return None @staticmethod def resolve_review_status(obj: Rule): return 'reviewed' if obj.package.reviewed else 'unreviewed' @staticmethod def resolve_product_filter_smarts(obj: Rule): return obj.product_filter_smarts if obj.product_filter_smarts else '' @staticmethod def resolve_reactant_filter_smarts(obj: Rule): return obj.reactant_filter_smarts if obj.reactant_filter_smarts else '' @router.get("/rule", response={200: RuleWrapper, 403: Error}) def get_rules(request): qs = Rule.objects.none() for p in PackageManager.get_reviewed_packages(): qs |= Rule.objects.filter(package=p) return {'rule': qs} @router.get("/package/{uuid:package_uuid}/rule", response={200: RuleWrapper, 403: Error}) def get_package_rules(request, package_uuid): try: p = PackageManager.get_package_by_id(request.user, package_uuid) return {'rule': Rule.objects.filter(package=p).prefetch_related('package')} except ValueError: return 403, { 'message': f'Getting Rules for Package with id {package_uuid} failed due to insufficient rights!'} @router.get("/package/{uuid:package_uuid}/rule/{uuid:rule_uuid}", response={200: SimpleRuleSchema | CompositeRuleSchema, 403: Error}) def get_package_rule(request, package_uuid, rule_uuid): return _get_package_rule(request, package_uuid, rule_uuid) @router.get("/package/{uuid:package_uuid}/simple-rule/{uuid:rule_uuid}", response={200: SimpleRuleSchema | CompositeRuleSchema, 403: Error}) def get_package_simple_rule(request, package_uuid, rule_uuid): return _get_package_rule(request, package_uuid, rule_uuid) @router.get("/package/{uuid:package_uuid}/parallel-rule/{uuid:rule_uuid}", response={200: SimpleRuleSchema | CompositeRuleSchema, 403: Error}) def get_package_parallel_rule(request, package_uuid, rule_uuid): return _get_package_rule(request, package_uuid, rule_uuid) def _get_package_rule(request, package_uuid, rule_uuid): try: p = PackageManager.get_package_by_id(request.user, package_uuid) return Rule.objects.get(package=p, uuid=rule_uuid) except ValueError: return 403, { 'message': f'Getting Rule with id {rule_uuid} failed due to insufficient rights!'} # POST @router.post("/package/{uuid:package_uuid}/rule/{uuid:rule_uuid}", response={200: str | Any, 403: Error}) def post_package_rule(request, package_uuid, rule_uuid, compound: Form[str] = None): return _post_package_rule(request, package_uuid, rule_uuid, compound=compound) @router.post("/package/{uuid:package_uuid}/simple-rule/{uuid:rule_uuid}", response={200: str | Any, 403: Error}) def post_package_simple_rule(request, package_uuid, rule_uuid, compound: Form[str] = None): return _post_package_rule(request, package_uuid, rule_uuid, compound=compound) @router.post("/package/{uuid:package_uuid}/parallel-rule/{uuid:rule_uuid}", response={200: str | Any, 403: Error}) def post_package_parallel_rule(request, package_uuid, rule_uuid, compound: Form[str] = None): return _post_package_rule(request, package_uuid, rule_uuid, compound=compound) def _post_package_rule(request, package_uuid, rule_uuid, compound: Form[str]): try: p = PackageManager.get_package_by_id(request.user, package_uuid) r = Rule.objects.get(package=p, uuid=rule_uuid) if compound is not None: if not compound.split(): return 400, {'message': 'Compound is empty'} product_sets = r.apply(compound) res = [] for p_set in product_sets: for product in p_set: res.append(product) return HttpResponse('\n'.join(res), content_type="text/plain") return r except ValueError: return 403, { 'message': f'Getting Rule with id {rule_uuid} failed due to insufficient rights!'} ############ # Reaction # ############ class SimpleReaction(SimpleObject): identifier: str = 'reaction' class ReactionWrapper(Schema): reaction: List['SimpleReaction'] class ReactionCompoundStructure(Schema): compoundName: str = Field(None, alias="name") id: str = Field(None, alias="url") smiles: str = Field(None, alias="smiles") class ReactionSchema(Schema): aliases: List[str] = Field([], alias="aliases") description: str = Field(None, alias="description") ecNumbers: List[Dict[str, str]] = Field([], alias="ec_numbers") educts: List['ReactionCompoundStructure'] = Field([], alias="educts") id: str = Field(None, alias="url") identifier: str = 'reaction' medlineRefs: List[str] = Field([], alias="medline_references") multistep: bool = Field(None, alias="multi_step") name: str = Field(None, alias="name") pathways: List['SimplePathway'] = Field([], alias="related_pathways") products: List['ReactionCompoundStructure'] = Field([], alias="products") references: List[Dict[str, List[str]]] = Field([], alias="references") reviewStatus: str = Field(None, alias="review_status") scenarios: List['SimpleScenario'] = Field([], alias="scenarios") smirks: str = Field("", alias="smirks") @staticmethod def resolve_smirks(obj: Reaction): return obj.smirks() @staticmethod def resolve_ec_numbers(obj: Reaction): # TODO fetch via scenario EnzymeAI return [] @staticmethod def resolve_references(obj: Reaction): # TODO return [] @staticmethod def resolve_medline_references(obj: Reaction): # TODO return [] @staticmethod def resolve_review_status(obj: Rule): return 'reviewed' if obj.package.reviewed else 'unreviewed' @router.get("/reaction", response={200: ReactionWrapper, 403: Error}) def get_reactions(request): qs = Reaction.objects.none() for p in PackageManager.get_reviewed_packages(): qs |= Reaction.objects.filter(package=p) return {'reaction': qs} @router.get("/package/{uuid:package_uuid}/reaction", response={200: ReactionWrapper, 403: Error}) def get_package_reactions(request, package_uuid): try: p = PackageManager.get_package_by_id(request.user, package_uuid) return {'reaction': Reaction.objects.filter(package=p).prefetch_related('package')} except ValueError: return 403, { 'message': f'Getting Reactions for Package with id {package_uuid} failed due to insufficient rights!'} @router.get("/package/{uuid:package_uuid}/reaction/{uuid:reaction_uuid}", response={200: ReactionSchema, 403: Error}) def get_package_reaction(request, package_uuid, reaction_uuid): try: p = PackageManager.get_package_by_id(request.user, package_uuid) return Reaction.objects.get(package=p, uuid=reaction_uuid) except ValueError: return 403, { 'message': f'Getting Reaction with id {reaction_uuid} failed due to insufficient rights!'} ############ # Scenario # ############ class SimpleScenario(SimpleObject): identifier: str = 'scenario' class ScenarioWrapper(Schema): scenario: List['SimpleScenario'] class ScenarioSchema(Schema): aliases: List[str] = Field([], alias="aliases") collection: Dict['str', List[Dict[str, Any]]] = Field([], alias="collection") collectionID: Optional[str] = None description: str = Field(None, alias="description") id: str = Field(None, alias="url") identifier: str = 'scenario' linkedTo: List[Dict[str, str]] = Field({}, alias="linked_to") name: str = Field(None, alias="name") pathways: List['SimplePathway'] = Field([], alias="related_pathways") relatedScenarios: List[Dict[str, str]] = Field([], alias="related_scenarios") reviewStatus: str = Field(None, alias="review_status") scenarios: List['SimpleScenario'] = Field([], alias="scenarios") type: str = Field(None, alias="scenario_type") @staticmethod def resolve_collection(obj: Scenario): return obj.additional_information @staticmethod def resolve_review_status(obj: Rule): return 'reviewed' if obj.package.reviewed else 'unreviewed' @router.get("/scenario", response={200: ScenarioWrapper, 403: Error}) def get_scenarios(request): qs = Scenario.objects.none() for p in PackageManager.get_reviewed_packages(): qs |= Scenario.objects.filter(package=p) return {'scenario': qs} @router.get("/package/{uuid:package_uuid}/scenario", response={200: ScenarioWrapper, 403: Error}) def get_package_scenarios(request, package_uuid): try: p = PackageManager.get_package_by_id(request.user, package_uuid) return {'scenario': Scenario.objects.filter(package=p).prefetch_related('package')} except ValueError: return 403, { 'message': f'Getting Scenarios for Package with id {package_uuid} failed due to insufficient rights!'} @router.get("/package/{uuid:package_uuid}/scenario/{uuid:scenario_uuid}", response={200: ScenarioSchema, 403: Error}) def get_package_scenario(request, package_uuid, scenario_uuid): try: p = PackageManager.get_package_by_id(request.user, package_uuid) return Scenario.objects.get(package=p, uuid=scenario_uuid) except ValueError: return 403, { 'message': f'Getting Scenario with id {scenario_uuid} failed due to insufficient rights!'} ########### # Pathway # ########### class SimplePathway(SimpleObject): identifier: str = 'pathway' class PathwayWrapper(Schema): pathway: List['SimplePathway'] @router.get("/pathway", response={200: PathwayWrapper, 403: Error}) def get_pathways(request): qs = Pathway.objects.none() for p in PackageManager.get_reviewed_packages(): qs |= Pathway.objects.filter(package=p) return {'pathway': qs} @router.get("/package/{uuid:package_uuid}/pathway", response={200: PathwayWrapper, 403: Error}) def get_package_pathways(request, package_uuid): try: p = PackageManager.get_package_by_id(request.user, package_uuid) return {'pathway': Pathway.objects.filter(package=p).prefetch_related('package')} except ValueError: return 403, { 'message': f'Getting Pathways for Package with id {package_uuid} failed due to insufficient rights!'} # @router.get("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}", response={200: Pathway, 403: Error}) # def get_package_pathway(request, package_uuid, pathway_uuid): # try: # p = PackageManager.get_package_by_id(request.user, package_uuid) # return Pathway.objects.get(package=p, uuid=pathway_uuid) # except ValueError: # return 403, { # 'message': f'Getting Pathway with id {pathway_uuid} failed due to insufficient rights!'}