diff --git a/README.md b/README.md index 242e937..e4c79fc 100644 --- a/README.md +++ b/README.md @@ -36,13 +36,23 @@ can be found below. - update your dependencies ```shell conda install --file requirements/run.txt + pip install -r requirements/pip_requirements.txt ``` +- Currently, there are three usernames and passwords required in order to develop the website. One for oauth (env var), + mongo atlas (yaml file opened in script), for google cloud storage (json file pointed at by ENV var). The latter can + be ascertained as described above. The former two have been described below. - Add a secret username and password to a yml file in the pydatarecognition folder named secret_password.yml - These should take the following form (you replace the <>, removing the <>) ```yaml username: password: ``` +- Add an oauth login page to your google cloud platform account () and add the following variables to a .env file in the + pydatarecognition directory + ```shell + GOOGLE_CLIENT_ID= + GOOGLE_CLIENT_SECRET= + ``` - run the following command from the base dir terminal to run the app ```shell uvicorn pydatarecognition.app:app --reload diff --git a/pydatarecognition/app.py b/pydatarecognition/app.py index 24b1cc1..51066ca 100644 --- a/pydatarecognition/app.py +++ b/pydatarecognition/app.py @@ -1,143 +1,205 @@ import os from pathlib import Path -import yaml -import tempfile -import shutil -import uuid - -from fastapi import FastAPI, Body, HTTPException, status, File -from fastapi.responses import JSONResponse -from fastapi.encoders import jsonable_encoder -from typing import List, Optional, Literal -import motor.motor_asyncio -from pydatarecognition.powdercif import PydanticPowderCif -from pydatarecognition.utils import xy_resample -from pydatarecognition.cif_io import user_input_read -from skbeam.core.utils import twotheta_to_q -import scipy.stats -import numpy as np +import io +import base64 +from functools import wraps -STEPSIZE_REGULAR_QGRID = 10**-3 +from fastapi import FastAPI, File, Form, Depends +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from fastapi.openapi.docs import get_swagger_ui_html + +from starlette.config import Config as Configure +from starlette.requests import Request +from starlette.middleware.sessions import SessionMiddleware +from starlette.responses import RedirectResponse + +from authlib.integrations.starlette_client import OAuth -COLLECTION = "cif" +from typing import Optional, Literal -app = FastAPI() +import pydatarecognition.rest_api as rest_api +from pydatarecognition.dependencies import get_user +from pydatarecognition.mongo_client import mongo_client +from pydatarecognition.rank import rank_db_cifs +from pydatarecognition.cif_io import rank_write -# Connect to mongodb filepath = Path(os.path.abspath(__file__)) -with open(os.path.join(filepath.parent, 'secret_password.yml'), 'r') as f: - user_secrets = yaml.safe_load(f) -username = user_secrets['username'] -password = user_secrets['password'] -client = motor.motor_asyncio.AsyncIOMotorClient(f'mongodb+srv://{username}:{password}@sidewinder.uc5ro.mongodb.net/?retryWrites=true&w=majority') -db = client.test -# Setup cif mapping reference -CIF_DIR = filepath.parent.parent / 'docs' / 'examples' / 'cifs' -doifile = CIF_DIR / 'iucrid_doi_mapping.txt' -dois = np.genfromtxt(doifile, dtype='str') -doi_dict = {} -for i in range(len(dois)): - doi_dict[dois[i][0]] = dois[i][1] +STEPSIZE_REGULAR_QGRID = 10**-3 +app = FastAPI(docs_url=None, redoc_url=None) +app.add_event_handler("startup", mongo_client.connect_db) +app.add_event_handler("shutdown", mongo_client.close_mongo_connection) +app.include_router(rest_api.router) +app.mount("/static", StaticFiles(directory="static"), name="static") +app.add_middleware(SessionMiddleware, secret_key='!secret') +templates = Jinja2Templates(directory="templates") + + +def login_required(f): + @wraps(f) + async def wrapped(request, *args, **kwargs): + if request.session.get('login_status'): + if request.session['login_status'] == "authorized": + return await f(request, *args, **kwargs) + return RedirectResponse(app.url_path_for('login')) + return wrapped + + +@app.route('/') +async def home(request: Request): + if request.session.get('login_status'): + if request.session['login_status'] == "authorized": + return templates.TemplateResponse('landing.html', + {"request": request, "user": request.session.get('username'), "img": request.session.get('photourl')}) + else: + return templates.TemplateResponse('landing.html', {"request": request, "user": None}) + else: + return templates.TemplateResponse('landing.html', {"request": request, "user": None}) + + +@app.route('/about') +def footer_about(request: Request): + """ + Route function for about in the footer. + + Returns + ------- + render_template + Renders the footer-about page. + """ + return templates.TemplateResponse('footer-about.html', + {"request": request, "user": request.session.get('username'), "img": request.session.get('photourl')}) + + +@app.route('/privacy') +def footer_privacy(request: Request): + """ + Route function for privacy policy in the footer. + + Returns + ------- + render_template + Renders the privacy-policy page. + """ + return templates.TemplateResponse('footer-privacy.html', + {"request": request, "user": request.session.get('username'), "img": request.session.get('photourl')}) + + +@app.route('/terms') +async def footer_term(request: Request): + """ + Route function for terms of use in the footer. + + Returns + ------- + render_template + Renders the footer-term page. + """ + return templates.TemplateResponse('footer-term.html', + {"request": request, "user": request.session.get('username'), "img": request.session.get('photourl')}) + + +# Initialize our OAuth instance from the client ID and client secret specified in our .env file +config = Configure('.env') +oauth = OAuth(config) + +CONF_URL = 'https://accounts.google.com/.well-known/openid-configuration' +oauth.register( + name='google', + server_metadata_url=CONF_URL, + client_kwargs={ + 'scope': 'openid email profile' + } +) +@app.get('/login', tags=['authentication']) # Tag it as "authentication" for our docs +async def login(request: Request): -@app.post("/", response_description="Add new CIF", response_model=PydanticPowderCif) -async def create_cif(powdercif: PydanticPowderCif = Body(...)): - powdercif = jsonable_encoder(powdercif) - new_cif = await db[COLLECTION].insert_one(powdercif) - created_cif = await db[COLLECTION].find_one({"_id": new_cif.inserted_id}) - return JSONResponse(status_code=status.HTTP_201_CREATED, content=created_cif) + return templates.TemplateResponse('login.html', {"request": request, "user": None}) -@app.get( - "/", response_description="List all cifs", response_model=List[PydanticPowderCif] -) -async def list_cifs(): - cifs = await db[COLLECTION].find().to_list(5) - return cifs +@app.get('/google_login', tags=['authentication']) # Tag it as "authentication" for our docs +async def google_login(request: Request): + # Redirect Google OAuth back to our application + redirect_uri = request.url_for('auth') + return await oauth.google.authorize_redirect(request, redirect_uri) -@app.get( - "/{id}", response_description="Get a single CIF", response_model=PydanticPowderCif -) -async def show_cif(id: str): - if (cif := await db[COLLECTION].find_one({"_id": id})) is not None: - return cif - raise HTTPException(status_code=404, detail=f"CIF {id} not found") +@app.route('/auth') +async def auth(request: Request): + # Perform Google OAuth + token = await oauth.google.authorize_access_token(request) + user = await oauth.google.parse_id_token(request, token) + # Save the user + request.session['user'] = dict(user) + request.session['photourl'] = request.session['user']['picture'] + request.session['username'] = request.session['user']['given_name'] if ('given_name' in request.session['user']) else "Anonymous" + request.session['login_status'] = 'authorized' -@app.put("/{id}", response_description="Update a CIF", response_model=PydanticPowderCif) -async def update_cif(id: str, cif: PydanticPowderCif = Body(...)): - cif = {k: v for k, v in cif.dict().items() if v is not None} + return RedirectResponse(url='/') - if len(cif) >= 1: - update_result = await db[COLLECTION].update_one({"_id": id}, {"$set": cif}) - if update_result.modified_count == 1: - if ( - updated_cif := await db[COLLECTION].find_one({"_id": id}) - ) is not None: - return updated_cif +@app.get('/logout', tags=['authentication']) # Tag it as "authentication" for our docs +async def logout(request: Request): + # Remove the user + request.session.pop('user', None) + request.session.pop('photourl', None) + request.session.pop('username', None) + request.session.pop('login_status', None) - if (existing_cif := await db[COLLECTION].find_one({"_id": id})) is not None: - return existing_cif + return RedirectResponse(url='/') - raise HTTPException(status_code=404, detail=f"CIF {id} not found") +@app.route('/docs', methods=['GET']) # Tag it as "documentation" for our docs +@login_required +async def get_documentation(request: Request): + response = get_swagger_ui_html(openapi_url='/openapi.json', title='Documentation') + return response -@app.delete("/{id}", response_description="Delete a CIF") -async def delete_cif(id: str): - delete_result = await db[COLLECTION].delete_one({"_id": id}) - if delete_result.deleted_count == 1: - return JSONResponse(status_code=status.HTTP_204_NO_CONTENT) +@app.route('/cif_search', methods=['GET']) +@login_required +async def cif_search(request: Request): + """ + Route function for cif search. - raise HTTPException(status_code=404, detail=f"CIF {id} not found") + Returns + ------- + render_template + Renders the cif search page. + """ + return templates.TemplateResponse('cif_search.html', + {"request": request, "user": request.session.get('username'), + "img": request.session.get('photourl'), + "result": None + }) -@app.put( - "/query/", response_description="Rank matches to User Input Data" -) -async def rank_cif(xtype: Literal["twotheta", "q"], wavelength: float, user_input: bytes = File(...), paper_filter_iucrid: Optional[str] = None): - cifname_ranks = [] - r_pearson_ranks = [] - doi_ranks = [] - tempdir = tempfile.mkdtemp() - temp_filename = os.path.join(tempdir, f'temp_{uuid.uuid4()}.txt') - with open(temp_filename, 'wb') as w: - w.write(user_input) - userdata = user_input_read(temp_filename) - user_x_data, user_intensity = userdata[0, :], userdata[1:, ][0] - if xtype == 'twotheta': - user_q = twotheta_to_q(np.radians(user_x_data), wavelength) - if paper_filter_iucrid: - cif_list = db[COLLECTION].find({"iucrid": paper_filter_iucrid}) - else: - cif_list = db[COLLECTION].find({}) - async for cif in cif_list: - mongo_cif = PydanticPowderCif(**cif) - try: - data_resampled = xy_resample(user_q, user_q, mongo_cif.q, mongo_cif.intensity, STEPSIZE_REGULAR_QGRID) - pearson = scipy.stats.pearsonr(data_resampled[0][:, 1], data_resampled[1][:, 1]) - r_pearson = pearson[0] - p_pearson = pearson[1] - cifname_ranks.append(mongo_cif.cif_file_name) - r_pearson_ranks.append(r_pearson) - doi = doi_dict[mongo_cif.iucrid] - doi_ranks.append(doi) - except AttributeError: - print(f"{mongo_cif.cif_file_name} was skipped.") - - cif_rank_pearson = sorted(list(zip(cifname_ranks, r_pearson_ranks, doi_ranks)), key=lambda x: x[1], reverse=True) - ranks = [{'IUCrCIF': cif_rank_pearson[i][0], - 'score': cif_rank_pearson[i][1], - 'doi': cif_rank_pearson[i][2]} for i in range(len(cif_rank_pearson))] - shutil.rmtree(tempdir) - return ranks + +@app.post('/cif_search', tags=['Web Interface']) +async def upload_data_cif(request: Request, user_input: bytes = File(...), wavelength: str = Form(...), + filter_key: str = Form(None), filter_value: str = Form(None), + datatype: Literal["twotheta", "q"] = Form(...), user: Optional[dict] = Depends(get_user)): + db_client = await mongo_client.get_db_client() + db = db_client.test + ranks, plot = await rank_db_cifs(db, datatype, wavelength, user_input, filter_key, filter_value, plot=True) + # creates an in-memory buffer in which to store the file + file_object = io.BytesIO() + plot.savefig(file_object, format='png', dpi=150) + base64img = "data:image/png;base64," + base64.b64encode(file_object.getvalue()).decode('ascii') + result = rank_write(ranks).replace('\t\t', '     ') + return templates.TemplateResponse('cif_search.html', + {"request": request, "user": request.session.get('username'), + "img": request.session.get('photourl'), + "result": result.replace('\t', '  '), + "base64img": base64img + }) if __name__ == "__main__": import uvicorn - uvicorn.run("app:app", host="localhost", reload=True) \ No newline at end of file + uvicorn.run("app:app", host="127.0.0.1", port=8000, reload=True) \ No newline at end of file diff --git a/pydatarecognition/cif_io.py b/pydatarecognition/cif_io.py index 925514f..352a80c 100644 --- a/pydatarecognition/cif_io.py +++ b/pydatarecognition/cif_io.py @@ -115,7 +115,7 @@ def user_input_read(user_input_file_path): return user_data -def rank_write(cif_ranks, output_path): +def rank_write(cif_ranks, output_path=None): ''' given a list of dicts of IUCr CIFs, scores, and DOIs together with a path to the output dir, writes a .txt file with ranks, scores, IUCr CIFs, and DOIs. @@ -153,10 +153,11 @@ def rank_write(cif_ranks, output_path): f"{encoded_ref_string}\n" rank_doi_score_txt_print += f"{i+1}{tab_char*2}{cif_ranks[i]['score']:.4f}\t{cif_ranks[i]['doi']}\t" \ f"{encoded_ref_string}\n" - with open(output_path / 'rank_WindowsNotepad.txt', 'w') as output_file: - output_file.write(rank_doi_score_txt_write) - with open(output_path / 'rank_PyCharm_Notepad++.txt', 'w') as output_file: - output_file.write(rank_doi_score_txt_print) + if output_path: + with open(output_path / 'rank_WindowsNotepad.txt', 'w') as output_file: + output_file.write(rank_doi_score_txt_write) + with open(output_path / 'rank_PyCharm_Notepad++.txt', 'w') as output_file: + output_file.write(rank_doi_score_txt_print) return rank_doi_score_txt_print diff --git a/pydatarecognition/dependencies.py b/pydatarecognition/dependencies.py new file mode 100644 index 0000000..f87d559 --- /dev/null +++ b/pydatarecognition/dependencies.py @@ -0,0 +1,14 @@ +from typing import Optional +from starlette.requests import Request +from fastapi import HTTPException + + +# Try to get the logged in user +async def get_user(request: Request) -> Optional[dict]: + user = request.session.get('user', None) + if user is not None: + return user + else: + raise HTTPException(status_code=403, detail='Please return to home screen and log in.') + + return None \ No newline at end of file diff --git a/pydatarecognition/mongo_client.py b/pydatarecognition/mongo_client.py new file mode 100644 index 0000000..f2aa030 --- /dev/null +++ b/pydatarecognition/mongo_client.py @@ -0,0 +1,36 @@ +from pathlib import Path +import yaml +import os + +from motor.motor_asyncio import AsyncIOMotorClient + +filepath = Path(os.path.abspath(__file__)) + + + +# Connect to mongodb + +with open(os.path.join(filepath.parent, 'secret_password.yml'), 'r') as f: + user_secrets = yaml.safe_load(f) +username = user_secrets['username'] +password = user_secrets['password'] + + +class MongoClient(): + db_client: AsyncIOMotorClient = None + + async def get_db_client(self) -> AsyncIOMotorClient: + """Return database client instance.""" + return self.db_client + + + async def connect_db(self): + """Create database connection.""" + self.db_client = AsyncIOMotorClient( + f'mongodb+srv://{username}:{password}@sidewinder.uc5ro.mongodb.net/?retryWrites=true&w=majority') + + async def close_mongo_connection(self): + """Close database connection.""" + self.db_client.close() + +mongo_client = MongoClient() diff --git a/pydatarecognition/mongo_utils.py b/pydatarecognition/mongo_utils.py index 32f6fd9..68c63ac 100644 --- a/pydatarecognition/mongo_utils.py +++ b/pydatarecognition/mongo_utils.py @@ -34,6 +34,7 @@ def cifs_to_mongo(mongo_db_uri: str, mongo_db_name: str, mongo_collection_name: import yaml from google.cloud import storage + from google.cloud.exceptions import Conflict filepath = Path(os.path.abspath(__file__)) @@ -41,7 +42,10 @@ def cifs_to_mongo(mongo_db_uri: str, mongo_db_name: str, mongo_collection_name: os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = os.path.join(filepath.parent.absolute(), '../requirements/testing-cif-datarec-secret.json') storage_client = storage.Client() - storage_client.create_bucket('raw_cif_data') + try: + storage_client.create_bucket('raw_cif_data') + except Conflict: + pass CIF_DIR = filepath.parent.parent / 'docs' / 'examples' / 'cifs' with open('secret_password.yml', 'r') as f: secret_dict = yaml.safe_load(f) diff --git a/pydatarecognition/plotters.py b/pydatarecognition/plotters.py index 88d3e55..01d2d84 100644 --- a/pydatarecognition/plotters.py +++ b/pydatarecognition/plotters.py @@ -44,7 +44,7 @@ def iinvd_plot(inv_d, i): return id_plot -def rank_plot(q_reg, userdata_resampled_int, cif_rank_pearson_list, cif_dict, png_path): +def rank_plot(q_reg, userdata_resampled_int, cif_rank_pearson_list, cif_dict, png_path=None): cifdata_q_reg, cifdata_resampled_intensity = [], [] for i in range(0, 5): file = cif_rank_pearson_list[i][0] @@ -77,6 +77,8 @@ def rank_plot(q_reg, userdata_resampled_int, cif_rank_pearson_list, cif_dict, pn axs[i].text(0.875 * x_max, 0.65 * y_max, f'Rank: {i}') axs[i].set_yticks([]) axs[i].set_ylim(y_min - 0.1*y_range, y_max + 0.1*y_range) - plt.savefig(png_path / 'rank_plot.png', bbox_inches='tight') - + if png_path: + plt.savefig(png_path / 'rank_plot.png', bbox_inches='tight') + else: + return fig return None diff --git a/pydatarecognition/powdercif.py b/pydatarecognition/powdercif.py index 2e32c49..3974e14 100644 --- a/pydatarecognition/powdercif.py +++ b/pydatarecognition/powdercif.py @@ -2,15 +2,19 @@ from io import BytesIO import uuid from pathlib import Path -from functools import lru_cache +# from functools import lru_cache import re +import asyncio import numpy as np from skbeam.core.utils import twotheta_to_q, q_to_twotheta from pydantic import Field, validator from odmantic.bson import BSON_TYPES_ENCODERS, BaseBSONModel, ObjectId from bson.errors import InvalidId -from google.cloud import storage +from gcloud.aio.storage import Storage +from google.cloud import storage as sync_storage + +MODEL_VERSION = '0.0.1' filepath = Path(os.path.abspath(__file__)) if os.path.isfile(os.path.join(filepath.parent.absolute(), '../requirements/testing-cif-datarec-secret.json')): @@ -24,12 +28,17 @@ BUCKET_NAME = 'raw_cif_data' DAYS_CACHED = 5 +GCS_METADATA = { + 'pydantic_powder_model_version': MODEL_VERSION, + 'numpy_version': np._version.get_versions()['version'], + 'format': '.npy' + } try: # Will only work in python 3.8 and up - from typing import Optional, Literal, get_args, Any + from typing import Optional, Literal, get_args, Any, Union except: - from typing import Optional, Any + from typing import Optional, Any, Union from typing_extensions import Literal, get_args allowed_lengths = Literal["ang", "angs", "angstroms", "nm", "nanometers"] @@ -73,18 +82,56 @@ def validate_type(cls, val): return np.array(val, dtype=dtype) -def export_to_gcs(array: np.ndarray): - storage_client = storage.Client() +async def export_to_gcs(array: np.ndarray) -> str: + """ + Async export to gcs, can be utilized if we define a class method to export data pre-emptively + @param array: + @return: gcs_token + """ + async with Storage() as storage_client: + file_id = uuid.uuid4().hex + out = BytesIO() + np.save(out, array) + out.seek(0) + status = await storage_client.upload(BUCKET_NAME, file_id, out, metadata={ + 'cache_control': f"public, max-age={int(DAYS_CACHED*60*60*24)}", + 'metadata': GCS_METADATA + }) + print(status) + return file_id + + +def json_gcs_export(array: np.ndarray) -> str: + """ + json encoder cannot be defined with async calls, therefore the normal google cloud client is utilized + @param array: + @return: gcs_token + """ + storage_client = sync_storage.Client() cif_bucket = storage_client.get_bucket(BUCKET_NAME) file_id = uuid.uuid4().hex blob = cif_bucket.blob(file_id) out = BytesIO() np.save(out, array) out.seek(0) + blob.cache_control = f"public, max-age={int(DAYS_CACHED*60*60*24)}" + blob.metadata = GCS_METADATA blob.upload_from_file(out) + storage_client.close() return file_id +# Each is ~56KB, 1000 of which are 0.56GB of RAM +#TODO implement a cache when we have meaningful filerting. Normal LRu cache below will not work async +# @lru_cache(maxsize=1000, typed=True) +async def retrieve_glob_as_np(uid: str) -> np.ndarray: + async with Storage() as storage_client: + file = await storage_client.download(BUCKET_NAME, uid) + buffer = BytesIO(file) + buffer.seek(0) + return np.load(buffer) + + class PydanticPowderCif(BaseBSONModel): """Pydantic model of CIF Powder data for mongo database. Ingests CIF data and mongo data.""" iucrid: Optional[str] = Field(None, description="The Unique Identifier of the Paper that is Associated With " @@ -93,9 +140,15 @@ class PydanticPowderCif(BaseBSONModel): cif_file_name: Optional[str] = Field(None, description='Name of the file the cif originated from') wavelength: Optional[float] = Field(None, description='Wavelength of the Characterizing Radiation') wavel_units: allowed_lengths = Field(None, description='Wavelength units in nm') - q: Optional[Array] = Field(default_factory=list, description='Scattering Vector in Inverse nm') - ttheta: Optional[Array] = Field(default_factory=list, description='Scattering Angle in Radians') - intensity: Optional[Array] = Field(default_factory=list, description='Scattering Intensity') + q: Optional[Union[Array, str]] = Field(default_factory=list, description='Scattering Vector in Inverse nm') + ttheta: Optional[Union[Array, str]] = Field(default_factory=list, description='Scattering Angle in Radians') + intensity: Optional[Union[Array, str]] = Field(default_factory=list, description='Scattering Intensity') + model_version: str = Field( + MODEL_VERSION, + alias='schema-version', + description="Identifies the version of the pydantic powdercif model", + read_only=True + ) def __init__(self, iucrid=None, x_units: str = None, x=None, y=None, **data): if "_id" not in data and "id" not in data: @@ -158,29 +211,23 @@ class Config: underscore_attrs_are_private = False json_encoders = { **BSON_TYPES_ENCODERS, - np.ndarray: export_to_gcs, + np.ndarray: json_gcs_export, } - @validator('q', 'ttheta', 'intensity', pre=True) - def resolve_gcs_token(cls, val): - if isinstance(val, str): + async def resolve_gcs_tokens(self): + for array_token in ['q', 'ttheta', 'intensity']: + val = getattr(self, array_token) + if isinstance(val, str): + val_str = val + elif isinstance(val, np.ndarray): + try: + val_str = str(val) + except: + continue uuid4hex = re.compile('[0-9a-f]{32}\Z', re.I) - if uuid4hex.match(val): - val = retrieve_glob_as_np(val) - return val - return val - - -#each is ~56KB, 1000 of which are 0.56GB of RAM -@lru_cache(maxsize=1000, typed=True) -def retrieve_glob_as_np(uid: str) -> np.ndarray: - storage_client = storage.Client() - gcs_cif_bucket = storage_client.get_bucket(BUCKET_NAME) - blob = gcs_cif_bucket.blob(uid) - blob.cache_control = f"public, max-age={int(DAYS_CACHED*60*60*24)}" - buffer = BytesIO(blob.download_as_string()) - buffer.seek(0) - return np.load(buffer) + if uuid4hex.match(val_str): + array = await retrieve_glob_as_np(val_str) + setattr(self, array_token, array) class PowderCif: diff --git a/pydatarecognition/rank.py b/pydatarecognition/rank.py new file mode 100644 index 0000000..cc84f41 --- /dev/null +++ b/pydatarecognition/rank.py @@ -0,0 +1,117 @@ +import os +from pathlib import Path +import psutil +import tempfile +import uuid +import asyncio +from asyncio import BoundedSemaphore +from typing import Optional, Literal + +from motor.motor_asyncio import AsyncIOMotorClient + +import numpy as np + +from skbeam.core.utils import twotheta_to_q + +import scipy + +from pydatarecognition.cif_io import user_input_read +from pydatarecognition.powdercif import PydanticPowderCif +from pydatarecognition.utils import xy_resample +from pydatarecognition.plotters import rank_plot + +filepath = Path(os.path.abspath(__file__)) + +STEPSIZE_REGULAR_QGRID = 10**-3 + +COLLECTION = "cif" +MAX_MONGO_FIND = 1000000 + + +# Setup cif mapping reference +CIF_DIR = filepath.parent.parent / 'docs' / 'examples' / 'cifs' +doifile = CIF_DIR / 'iucrid_doi_mapping.txt' +dois = np.genfromtxt(doifile, dtype='str') +doi_dict = {} +for i in range(len(dois)): + doi_dict[dois[i][0]] = dois[i][1] + + +# Create an app level semaphore to prevent overloading the RAM. Assume ~100KB per cif, *5000 = 0.5GB +semaphore = BoundedSemaphore(5000) + + +async def rank_db_cifs(db: AsyncIOMotorClient, xtype: Literal["twotheta", "q"], wavelength: float, + user_input: bytes, filter_key: Optional[str] = None, filter_value: Optional[str] = None, + plot: bool = False): + cifname_ranks = [] + r_pearson_ranks = [] + doi_ranks = [] + cif_dict = {} + tempdir = tempfile.gettempdir() + file_name = f'temp_{uuid.uuid4()}.txt' + temp_filepath = os.path.join(tempdir, file_name) + with open(temp_filepath, 'wb') as w: + w.write(user_input) + userdata = user_input_read(temp_filepath) + user_x_data, user_intensity = userdata[0, :], userdata[1:, ][0] + if xtype == 'twotheta': + user_q = twotheta_to_q(np.radians(user_x_data), wavelength) + else: + user_q = user_x_data + if filter_key is not None and filter_value is not None: + cif_cursor = db[COLLECTION].find({filter_key: filter_value}) + else: + cif_cursor = db[COLLECTION].find({}) + mem_premongo = psutil.virtual_memory().percent + unpopulated_cif_list = await cif_cursor.to_list(length=MAX_MONGO_FIND) + mem_postmongo = psutil.virtual_memory().percent + print(f"Memory mongo_used in percent: {(mem_postmongo - mem_premongo)}") + futures = [limited_cif_load(cif) for cif in unpopulated_cif_list] + for future in asyncio.as_completed(futures): + mongo_cif = await future + try: + data_resampled = xy_resample(user_q, user_intensity, mongo_cif.q, mongo_cif.intensity, STEPSIZE_REGULAR_QGRID) + pearson = scipy.stats.pearsonr(data_resampled[0][:, 1], data_resampled[1][:, 1]) + r_pearson = pearson[0] + if plot: + p_pearson = pearson[1] + cifname_ranks.append(mongo_cif.cif_file_name) + r_pearson_ranks.append(r_pearson) + doi = doi_dict[mongo_cif.iucrid] + doi_ranks.append(doi) + if plot: + cif_dict[str(mongo_cif.cif_file_name)] = dict([ + ('intensity', mongo_cif.intensity), + ('q', mongo_cif.q), + ('qmin', np.amin(mongo_cif.q)), + ('qmax', np.amax(mongo_cif.q)), + ('q_reg', data_resampled[1][:, 0]), + ('intensity_resampled', data_resampled[1][:, 1]), + ('r_pearson', r_pearson), + ('p_pearson', p_pearson), + ('doi', doi), + ]) + except AttributeError: + print(f"{mongo_cif.cif_file_name} was skipped.") + loop_mem = psutil.virtual_memory().percent + print(f"Memory Used in loop in percent: {(loop_mem - mem_postmongo)}") + semaphore.release() + + cif_rank_pearson = sorted(list(zip(cifname_ranks, r_pearson_ranks, doi_ranks)), key=lambda x: x[1], reverse=True) + ranks = [{'IUCrCIF': cif_rank_pearson[i][0], + 'score': cif_rank_pearson[i][1], + 'doi': cif_rank_pearson[i][2]} for i in range(len(cif_rank_pearson))] + os.remove(temp_filepath) + if plot: + output_plot = rank_plot(data_resampled[0][:, 0], data_resampled[0][:, 1], cif_rank_pearson, cif_dict) + return ranks, output_plot + else: + return ranks + + +async def limited_cif_load(cif: dict): + await semaphore.acquire() + pcd = PydanticPowderCif(**cif) + await pcd.resolve_gcs_tokens() + return pcd diff --git a/pydatarecognition/rest_api.py b/pydatarecognition/rest_api.py new file mode 100644 index 0000000..5abeeb0 --- /dev/null +++ b/pydatarecognition/rest_api.py @@ -0,0 +1,110 @@ +import os +from pathlib import Path + +from fastapi import APIRouter, Body, HTTPException, status, File, Depends +from fastapi.responses import JSONResponse +from fastapi.openapi.utils import get_openapi + +from starlette.requests import Request + +from typing import List, Optional, Literal + +from pydatarecognition.powdercif import PydanticPowderCif +from pydatarecognition.dependencies import get_user +from pydatarecognition.mongo_client import mongo_client +from pydatarecognition.rank import rank_db_cifs + +import numpy as np + +filepath = Path(os.path.abspath(__file__)) + +STEPSIZE_REGULAR_QGRID = 10**-3 + +COLLECTION = "cif" +MAX_MONGO_FIND = 1000000 + + +# Setup cif mapping reference +CIF_DIR = filepath.parent.parent / 'docs' / 'examples' / 'cifs' +doifile = CIF_DIR / 'iucrid_doi_mapping.txt' +dois = np.genfromtxt(doifile, dtype='str') +doi_dict = {} +for i in range(len(dois)): + doi_dict[dois[i][0]] = dois[i][1] + + +router = APIRouter( + prefix="/API", + dependencies=[Depends(get_user)], + responses={404: {"description": "Not found"}}, +) + + +@router.route('/openapi.json') +async def get_open_api_endpoint(request: Request): # This dependency protects our endpoint! + response = JSONResponse(get_openapi(title='FastAPI', version=1, routes=router.routes)) + return response + + +@router.get( + "/{id}", response_description="Get a single CIF", response_model=PydanticPowderCif +) +async def show_cif(id: str): + db_client = await mongo_client.get_db_client() + db = db_client.test + if (cif := await db[COLLECTION].find_one({"_id": id})) is not None: + return cif + + raise HTTPException(status_code=404, detail=f"CIF {id} not found") + + +@router.put("/{id}", response_description="Update a CIF", response_model=PydanticPowderCif) +async def update_cif(id: str, cif: PydanticPowderCif = Body(...)): + db_client = await mongo_client.get_db_client() + db = db_client.test + cif = {k: v for k, v in cif.dict().items() if v is not None} + + if len(cif) >= 1: + update_result = await db[COLLECTION].update_one({"_id": id}, {"$set": cif}) + + if update_result.modified_count == 1: + if ( + updated_cif := await db[COLLECTION].find_one({"_id": id}) + ) is not None: + return updated_cif + + if (existing_cif := await db[COLLECTION].find_one({"_id": id})) is not None: + return existing_cif + + raise HTTPException(status_code=404, detail=f"CIF {id} not found") + + +@router.delete("/{id}", response_description="Delete a CIF") +async def delete_cif(id: str): + db_client = await mongo_client.get_db_client() + db = db_client.test + delete_result = await db[COLLECTION].delete_one({"_id": id}) + + if delete_result.deleted_count == 1: + return JSONResponse(status_code=status.HTTP_204_NO_CONTENT) + + raise HTTPException(status_code=404, detail=f"CIF {id} not found") + + +@router.get( + "/MQL/{filter_key}/{filter_criteria}", response_description="List filtered cifs", response_model=List[PydanticPowderCif] +) +async def list_cifs(filter_key: str, filter_criteria: str): + db_client = await mongo_client.get_db_client() + db = db_client.test + cifs = await db[COLLECTION].find({filter_key: filter_criteria}).to_list() + return cifs + + +@router.put( + "/rank/", response_description="Rank matches to User Input Data", tags=['rank'] +) +async def rank_cif(xtype: Literal["twotheta", "q"], wavelength: float, user_input: bytes = File(...), paper_filter_iucrid: Optional[str] = None): + db_client = await mongo_client.get_db_client() + db = db_client.test + return await rank_db_cifs(db, xtype, wavelength, user_input, "iucrid", paper_filter_iucrid, plot=False) diff --git a/pydatarecognition/static/css/bootstrap-social.css b/pydatarecognition/static/css/bootstrap-social.css new file mode 100644 index 0000000..2e4f6e1 --- /dev/null +++ b/pydatarecognition/static/css/bootstrap-social.css @@ -0,0 +1,153 @@ +/* + * Social Buttons for Bootstrap + * + * Copyright 2013-2016 Panayiotis Lipiridis + * Licensed under the MIT License + * + * https://github.com/lipis/bootstrap-social + */ + +.btn-social{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.btn-social>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)} +.btn-social.btn-lg{padding-left:61px}.btn-social.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em} +.btn-social.btn-sm{padding-left:38px}.btn-social.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em} +.btn-social.btn-xs{padding-left:30px}.btn-social.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em} +.btn-social-icon{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;height:34px;width:34px;padding:0}.btn-social-icon>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)} +.btn-social-icon.btn-lg{padding-left:61px}.btn-social-icon.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em} +.btn-social-icon.btn-sm{padding-left:38px}.btn-social-icon.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em} +.btn-social-icon.btn-xs{padding-left:30px}.btn-social-icon.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em} +.btn-social-icon>:first-child{border:none;text-align:center;width:100% !important} +.btn-social-icon.btn-lg{height:45px;width:45px;padding-left:0;padding-right:0} +.btn-social-icon.btn-sm{height:30px;width:30px;padding-left:0;padding-right:0} +.btn-social-icon.btn-xs{height:22px;width:22px;padding-left:0;padding-right:0} +.btn-adn{color:#fff;background-color:#d87a68;border-color:rgba(0,0,0,0.2)}.btn-adn:focus,.btn-adn.focus{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)} +.btn-adn:hover{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)} +.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active:hover,.btn-adn.active:hover,.open>.dropdown-toggle.btn-adn:hover,.btn-adn:active:focus,.btn-adn.active:focus,.open>.dropdown-toggle.btn-adn:focus,.btn-adn:active.focus,.btn-adn.active.focus,.open>.dropdown-toggle.btn-adn.focus{color:#fff;background-color:#b94630;border-color:rgba(0,0,0,0.2)} +.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{background-image:none} +.btn-adn.disabled:hover,.btn-adn[disabled]:hover,fieldset[disabled] .btn-adn:hover,.btn-adn.disabled:focus,.btn-adn[disabled]:focus,fieldset[disabled] .btn-adn:focus,.btn-adn.disabled.focus,.btn-adn[disabled].focus,fieldset[disabled] .btn-adn.focus{background-color:#d87a68;border-color:rgba(0,0,0,0.2)} +.btn-adn .badge{color:#d87a68;background-color:#fff} +.btn-bitbucket{color:#fff;background-color:#205081;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:focus,.btn-bitbucket.focus{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)} +.btn-bitbucket:hover{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)} +.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active:hover,.btn-bitbucket.active:hover,.open>.dropdown-toggle.btn-bitbucket:hover,.btn-bitbucket:active:focus,.btn-bitbucket.active:focus,.open>.dropdown-toggle.btn-bitbucket:focus,.btn-bitbucket:active.focus,.btn-bitbucket.active.focus,.open>.dropdown-toggle.btn-bitbucket.focus{color:#fff;background-color:#0f253c;border-color:rgba(0,0,0,0.2)} +.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{background-image:none} +.btn-bitbucket.disabled:hover,.btn-bitbucket[disabled]:hover,fieldset[disabled] .btn-bitbucket:hover,.btn-bitbucket.disabled:focus,.btn-bitbucket[disabled]:focus,fieldset[disabled] .btn-bitbucket:focus,.btn-bitbucket.disabled.focus,.btn-bitbucket[disabled].focus,fieldset[disabled] .btn-bitbucket.focus{background-color:#205081;border-color:rgba(0,0,0,0.2)} +.btn-bitbucket .badge{color:#205081;background-color:#fff} +.btn-dropbox{color:#fff;background-color:#1087dd;border-color:rgba(0,0,0,0.2)}.btn-dropbox:focus,.btn-dropbox.focus{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)} +.btn-dropbox:hover{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)} +.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active:hover,.btn-dropbox.active:hover,.open>.dropdown-toggle.btn-dropbox:hover,.btn-dropbox:active:focus,.btn-dropbox.active:focus,.open>.dropdown-toggle.btn-dropbox:focus,.btn-dropbox:active.focus,.btn-dropbox.active.focus,.open>.dropdown-toggle.btn-dropbox.focus{color:#fff;background-color:#0a568c;border-color:rgba(0,0,0,0.2)} +.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{background-image:none} +.btn-dropbox.disabled:hover,.btn-dropbox[disabled]:hover,fieldset[disabled] .btn-dropbox:hover,.btn-dropbox.disabled:focus,.btn-dropbox[disabled]:focus,fieldset[disabled] .btn-dropbox:focus,.btn-dropbox.disabled.focus,.btn-dropbox[disabled].focus,fieldset[disabled] .btn-dropbox.focus{background-color:#1087dd;border-color:rgba(0,0,0,0.2)} +.btn-dropbox .badge{color:#1087dd;background-color:#fff} +.btn-facebook{color:#fff;background-color:#3b5998;border-color:rgba(0,0,0,0.2)}.btn-facebook:focus,.btn-facebook.focus{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)} +.btn-facebook:hover{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)} +.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active:hover,.btn-facebook.active:hover,.open>.dropdown-toggle.btn-facebook:hover,.btn-facebook:active:focus,.btn-facebook.active:focus,.open>.dropdown-toggle.btn-facebook:focus,.btn-facebook:active.focus,.btn-facebook.active.focus,.open>.dropdown-toggle.btn-facebook.focus{color:#fff;background-color:#23345a;border-color:rgba(0,0,0,0.2)} +.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{background-image:none} +.btn-facebook.disabled:hover,.btn-facebook[disabled]:hover,fieldset[disabled] .btn-facebook:hover,.btn-facebook.disabled:focus,.btn-facebook[disabled]:focus,fieldset[disabled] .btn-facebook:focus,.btn-facebook.disabled.focus,.btn-facebook[disabled].focus,fieldset[disabled] .btn-facebook.focus{background-color:#3b5998;border-color:rgba(0,0,0,0.2)} +.btn-facebook .badge{color:#3b5998;background-color:#fff} +.btn-flickr{color:#fff;background-color:#ff0084;border-color:rgba(0,0,0,0.2)}.btn-flickr:focus,.btn-flickr.focus{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)} +.btn-flickr:hover{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)} +.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active:hover,.btn-flickr.active:hover,.open>.dropdown-toggle.btn-flickr:hover,.btn-flickr:active:focus,.btn-flickr.active:focus,.open>.dropdown-toggle.btn-flickr:focus,.btn-flickr:active.focus,.btn-flickr.active.focus,.open>.dropdown-toggle.btn-flickr.focus{color:#fff;background-color:#a80057;border-color:rgba(0,0,0,0.2)} +.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{background-image:none} +.btn-flickr.disabled:hover,.btn-flickr[disabled]:hover,fieldset[disabled] .btn-flickr:hover,.btn-flickr.disabled:focus,.btn-flickr[disabled]:focus,fieldset[disabled] .btn-flickr:focus,.btn-flickr.disabled.focus,.btn-flickr[disabled].focus,fieldset[disabled] .btn-flickr.focus{background-color:#ff0084;border-color:rgba(0,0,0,0.2)} +.btn-flickr .badge{color:#ff0084;background-color:#fff} +.btn-foursquare{color:#fff;background-color:#f94877;border-color:rgba(0,0,0,0.2)}.btn-foursquare:focus,.btn-foursquare.focus{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)} +.btn-foursquare:hover{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)} +.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active:hover,.btn-foursquare.active:hover,.open>.dropdown-toggle.btn-foursquare:hover,.btn-foursquare:active:focus,.btn-foursquare.active:focus,.open>.dropdown-toggle.btn-foursquare:focus,.btn-foursquare:active.focus,.btn-foursquare.active.focus,.open>.dropdown-toggle.btn-foursquare.focus{color:#fff;background-color:#e30742;border-color:rgba(0,0,0,0.2)} +.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{background-image:none} +.btn-foursquare.disabled:hover,.btn-foursquare[disabled]:hover,fieldset[disabled] .btn-foursquare:hover,.btn-foursquare.disabled:focus,.btn-foursquare[disabled]:focus,fieldset[disabled] .btn-foursquare:focus,.btn-foursquare.disabled.focus,.btn-foursquare[disabled].focus,fieldset[disabled] .btn-foursquare.focus{background-color:#f94877;border-color:rgba(0,0,0,0.2)} +.btn-foursquare .badge{color:#f94877;background-color:#fff} +.btn-github{color:#fff;background-color:#444;border-color:rgba(0,0,0,0.2)}.btn-github:focus,.btn-github.focus{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)} +.btn-github:hover{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)} +.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active:hover,.btn-github.active:hover,.open>.dropdown-toggle.btn-github:hover,.btn-github:active:focus,.btn-github.active:focus,.open>.dropdown-toggle.btn-github:focus,.btn-github:active.focus,.btn-github.active.focus,.open>.dropdown-toggle.btn-github.focus{color:#fff;background-color:#191919;border-color:rgba(0,0,0,0.2)} +.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{background-image:none} +.btn-github.disabled:hover,.btn-github[disabled]:hover,fieldset[disabled] .btn-github:hover,.btn-github.disabled:focus,.btn-github[disabled]:focus,fieldset[disabled] .btn-github:focus,.btn-github.disabled.focus,.btn-github[disabled].focus,fieldset[disabled] .btn-github.focus{background-color:#444;border-color:rgba(0,0,0,0.2)} +.btn-github .badge{color:#444;background-color:#fff} +.btn-google{color:#fff;background-color:#dd4b39;border-color:rgba(0,0,0,0.2)}.btn-google:focus,.btn-google.focus{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)} +.btn-google:hover{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)} +.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active:hover,.btn-google.active:hover,.open>.dropdown-toggle.btn-google:hover,.btn-google:active:focus,.btn-google.active:focus,.open>.dropdown-toggle.btn-google:focus,.btn-google:active.focus,.btn-google.active.focus,.open>.dropdown-toggle.btn-google.focus{color:#fff;background-color:#a32b1c;border-color:rgba(0,0,0,0.2)} +.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{background-image:none} +.btn-google.disabled:hover,.btn-google[disabled]:hover,fieldset[disabled] .btn-google:hover,.btn-google.disabled:focus,.btn-google[disabled]:focus,fieldset[disabled] .btn-google:focus,.btn-google.disabled.focus,.btn-google[disabled].focus,fieldset[disabled] .btn-google.focus{background-color:#dd4b39;border-color:rgba(0,0,0,0.2)} +.btn-google .badge{color:#dd4b39;background-color:#fff} +.btn-orcid{color:#fff;background-color:#a6ce39;border-color:rgba(0,0,0,0.2)}.btn-orcid:focus,.btn-orcid.focus{color:#fff;background-color:#89b01e;border-color:rgba(0,0,0,0.2)} +.btn-orcid:hover{color:#fff;background-color:#89b01e;border-color:rgba(0,0,0,0.2)} +.btn-orcid:active,.btn-orcid.active,.open>.dropdown-toggle.btn-orcid{color:#fff;background-color:#89b01e;border-color:rgba(0,0,0,0.2)}.btn-orcid:active:hover,.btn-orcid.active:hover,.open>.dropdown-toggle.btn-orcid:hover,.btn-orcid:active:focus,.btn-orcid.active:focus,.open>.dropdown-toggle.btn-orcid:focus,.btn-orcid:active.focus,.btn-orcid.active.focus,.open>.dropdown-toggle.btn-orcid.focus{color:#fff;background-color:#75961b;border-color:rgba(0,0,0,0.2)} +.btn-orcid:active,.btn-orcid.active,.open>.dropdown-toggle.btn-orcid{background-image:none} +.btn-orcid.disabled:hover,.btn-orcid[disabled]:hover,fieldset[disabled] .btn-orcid:hover,.btn-orcid.disabled:focus,.btn-orcid[disabled]:focus,fieldset[disabled] .btn-orcid:focus,.btn-orcid.disabled.focus,.btn-orcid[disabled].focus,fieldset[disabled] .btn-orcid.focus{background-color:#a6ce39;border-color:rgba(0,0,0,0.2)} +.btn-orcid .badge{color:#a6ce39;background-color:#fff} +.btn-instagram{color:#fff;background-color:#3f729b;border-color:rgba(0,0,0,0.2)}.btn-instagram:focus,.btn-instagram.focus{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)} +.btn-instagram:hover{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)} +.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active:hover,.btn-instagram.active:hover,.open>.dropdown-toggle.btn-instagram:hover,.btn-instagram:active:focus,.btn-instagram.active:focus,.open>.dropdown-toggle.btn-instagram:focus,.btn-instagram:active.focus,.btn-instagram.active.focus,.open>.dropdown-toggle.btn-instagram.focus{color:#fff;background-color:#26455d;border-color:rgba(0,0,0,0.2)} +.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{background-image:none} +.btn-instagram.disabled:hover,.btn-instagram[disabled]:hover,fieldset[disabled] .btn-instagram:hover,.btn-instagram.disabled:focus,.btn-instagram[disabled]:focus,fieldset[disabled] .btn-instagram:focus,.btn-instagram.disabled.focus,.btn-instagram[disabled].focus,fieldset[disabled] .btn-instagram.focus{background-color:#3f729b;border-color:rgba(0,0,0,0.2)} +.btn-instagram .badge{color:#3f729b;background-color:#fff} +.btn-linkedin{color:#fff;background-color:#007bb6;border-color:rgba(0,0,0,0.2)}.btn-linkedin:focus,.btn-linkedin.focus{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)} +.btn-linkedin:hover{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)} +.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active:hover,.btn-linkedin.active:hover,.open>.dropdown-toggle.btn-linkedin:hover,.btn-linkedin:active:focus,.btn-linkedin.active:focus,.open>.dropdown-toggle.btn-linkedin:focus,.btn-linkedin:active.focus,.btn-linkedin.active.focus,.open>.dropdown-toggle.btn-linkedin.focus{color:#fff;background-color:#00405f;border-color:rgba(0,0,0,0.2)} +.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{background-image:none} +.btn-linkedin.disabled:hover,.btn-linkedin[disabled]:hover,fieldset[disabled] .btn-linkedin:hover,.btn-linkedin.disabled:focus,.btn-linkedin[disabled]:focus,fieldset[disabled] .btn-linkedin:focus,.btn-linkedin.disabled.focus,.btn-linkedin[disabled].focus,fieldset[disabled] .btn-linkedin.focus{background-color:#007bb6;border-color:rgba(0,0,0,0.2)} +.btn-linkedin .badge{color:#007bb6;background-color:#fff} +.btn-microsoft{color:#fff;background-color:#2672ec;border-color:rgba(0,0,0,0.2)}.btn-microsoft:focus,.btn-microsoft.focus{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)} +.btn-microsoft:hover{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)} +.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active:hover,.btn-microsoft.active:hover,.open>.dropdown-toggle.btn-microsoft:hover,.btn-microsoft:active:focus,.btn-microsoft.active:focus,.open>.dropdown-toggle.btn-microsoft:focus,.btn-microsoft:active.focus,.btn-microsoft.active.focus,.open>.dropdown-toggle.btn-microsoft.focus{color:#fff;background-color:#0f4bac;border-color:rgba(0,0,0,0.2)} +.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{background-image:none} +.btn-microsoft.disabled:hover,.btn-microsoft[disabled]:hover,fieldset[disabled] .btn-microsoft:hover,.btn-microsoft.disabled:focus,.btn-microsoft[disabled]:focus,fieldset[disabled] .btn-microsoft:focus,.btn-microsoft.disabled.focus,.btn-microsoft[disabled].focus,fieldset[disabled] .btn-microsoft.focus{background-color:#2672ec;border-color:rgba(0,0,0,0.2)} +.btn-microsoft .badge{color:#2672ec;background-color:#fff} +.btn-odnoklassniki{color:#fff;background-color:#f4731c;border-color:rgba(0,0,0,0.2)}.btn-odnoklassniki:focus,.btn-odnoklassniki.focus{color:#fff;background-color:#d35b0a;border-color:rgba(0,0,0,0.2)} +.btn-odnoklassniki:hover{color:#fff;background-color:#d35b0a;border-color:rgba(0,0,0,0.2)} +.btn-odnoklassniki:active,.btn-odnoklassniki.active,.open>.dropdown-toggle.btn-odnoklassniki{color:#fff;background-color:#d35b0a;border-color:rgba(0,0,0,0.2)}.btn-odnoklassniki:active:hover,.btn-odnoklassniki.active:hover,.open>.dropdown-toggle.btn-odnoklassniki:hover,.btn-odnoklassniki:active:focus,.btn-odnoklassniki.active:focus,.open>.dropdown-toggle.btn-odnoklassniki:focus,.btn-odnoklassniki:active.focus,.btn-odnoklassniki.active.focus,.open>.dropdown-toggle.btn-odnoklassniki.focus{color:#fff;background-color:#b14c09;border-color:rgba(0,0,0,0.2)} +.btn-odnoklassniki:active,.btn-odnoklassniki.active,.open>.dropdown-toggle.btn-odnoklassniki{background-image:none} +.btn-odnoklassniki.disabled:hover,.btn-odnoklassniki[disabled]:hover,fieldset[disabled] .btn-odnoklassniki:hover,.btn-odnoklassniki.disabled:focus,.btn-odnoklassniki[disabled]:focus,fieldset[disabled] .btn-odnoklassniki:focus,.btn-odnoklassniki.disabled.focus,.btn-odnoklassniki[disabled].focus,fieldset[disabled] .btn-odnoklassniki.focus{background-color:#f4731c;border-color:rgba(0,0,0,0.2)} +.btn-odnoklassniki .badge{color:#f4731c;background-color:#fff} +.btn-openid{color:#fff;background-color:#f7931e;border-color:rgba(0,0,0,0.2)}.btn-openid:focus,.btn-openid.focus{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)} +.btn-openid:hover{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)} +.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active:hover,.btn-openid.active:hover,.open>.dropdown-toggle.btn-openid:hover,.btn-openid:active:focus,.btn-openid.active:focus,.open>.dropdown-toggle.btn-openid:focus,.btn-openid:active.focus,.btn-openid.active.focus,.open>.dropdown-toggle.btn-openid.focus{color:#fff;background-color:#b86607;border-color:rgba(0,0,0,0.2)} +.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{background-image:none} +.btn-openid.disabled:hover,.btn-openid[disabled]:hover,fieldset[disabled] .btn-openid:hover,.btn-openid.disabled:focus,.btn-openid[disabled]:focus,fieldset[disabled] .btn-openid:focus,.btn-openid.disabled.focus,.btn-openid[disabled].focus,fieldset[disabled] .btn-openid.focus{background-color:#f7931e;border-color:rgba(0,0,0,0.2)} +.btn-openid .badge{color:#f7931e;background-color:#fff} +.btn-pinterest{color:#fff;background-color:#cb2027;border-color:rgba(0,0,0,0.2)}.btn-pinterest:focus,.btn-pinterest.focus{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)} +.btn-pinterest:hover{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)} +.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active:hover,.btn-pinterest.active:hover,.open>.dropdown-toggle.btn-pinterest:hover,.btn-pinterest:active:focus,.btn-pinterest.active:focus,.open>.dropdown-toggle.btn-pinterest:focus,.btn-pinterest:active.focus,.btn-pinterest.active.focus,.open>.dropdown-toggle.btn-pinterest.focus{color:#fff;background-color:#801419;border-color:rgba(0,0,0,0.2)} +.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{background-image:none} +.btn-pinterest.disabled:hover,.btn-pinterest[disabled]:hover,fieldset[disabled] .btn-pinterest:hover,.btn-pinterest.disabled:focus,.btn-pinterest[disabled]:focus,fieldset[disabled] .btn-pinterest:focus,.btn-pinterest.disabled.focus,.btn-pinterest[disabled].focus,fieldset[disabled] .btn-pinterest.focus{background-color:#cb2027;border-color:rgba(0,0,0,0.2)} +.btn-pinterest .badge{color:#cb2027;background-color:#fff} +.btn-reddit{color:#000;background-color:#eff7ff;border-color:rgba(0,0,0,0.2)}.btn-reddit:focus,.btn-reddit.focus{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)} +.btn-reddit:hover{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)} +.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active:hover,.btn-reddit.active:hover,.open>.dropdown-toggle.btn-reddit:hover,.btn-reddit:active:focus,.btn-reddit.active:focus,.open>.dropdown-toggle.btn-reddit:focus,.btn-reddit:active.focus,.btn-reddit.active.focus,.open>.dropdown-toggle.btn-reddit.focus{color:#000;background-color:#98ccff;border-color:rgba(0,0,0,0.2)} +.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{background-image:none} +.btn-reddit.disabled:hover,.btn-reddit[disabled]:hover,fieldset[disabled] .btn-reddit:hover,.btn-reddit.disabled:focus,.btn-reddit[disabled]:focus,fieldset[disabled] .btn-reddit:focus,.btn-reddit.disabled.focus,.btn-reddit[disabled].focus,fieldset[disabled] .btn-reddit.focus{background-color:#eff7ff;border-color:rgba(0,0,0,0.2)} +.btn-reddit .badge{color:#eff7ff;background-color:#000} +.btn-soundcloud{color:#fff;background-color:#f50;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:focus,.btn-soundcloud.focus{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)} +.btn-soundcloud:hover{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)} +.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active:hover,.btn-soundcloud.active:hover,.open>.dropdown-toggle.btn-soundcloud:hover,.btn-soundcloud:active:focus,.btn-soundcloud.active:focus,.open>.dropdown-toggle.btn-soundcloud:focus,.btn-soundcloud:active.focus,.btn-soundcloud.active.focus,.open>.dropdown-toggle.btn-soundcloud.focus{color:#fff;background-color:#a83800;border-color:rgba(0,0,0,0.2)} +.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{background-image:none} +.btn-soundcloud.disabled:hover,.btn-soundcloud[disabled]:hover,fieldset[disabled] .btn-soundcloud:hover,.btn-soundcloud.disabled:focus,.btn-soundcloud[disabled]:focus,fieldset[disabled] .btn-soundcloud:focus,.btn-soundcloud.disabled.focus,.btn-soundcloud[disabled].focus,fieldset[disabled] .btn-soundcloud.focus{background-color:#f50;border-color:rgba(0,0,0,0.2)} +.btn-soundcloud .badge{color:#f50;background-color:#fff} +.btn-tumblr{color:#fff;background-color:#2c4762;border-color:rgba(0,0,0,0.2)}.btn-tumblr:focus,.btn-tumblr.focus{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)} +.btn-tumblr:hover{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)} +.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active:hover,.btn-tumblr.active:hover,.open>.dropdown-toggle.btn-tumblr:hover,.btn-tumblr:active:focus,.btn-tumblr.active:focus,.open>.dropdown-toggle.btn-tumblr:focus,.btn-tumblr:active.focus,.btn-tumblr.active.focus,.open>.dropdown-toggle.btn-tumblr.focus{color:#fff;background-color:#111c26;border-color:rgba(0,0,0,0.2)} +.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{background-image:none} +.btn-tumblr.disabled:hover,.btn-tumblr[disabled]:hover,fieldset[disabled] .btn-tumblr:hover,.btn-tumblr.disabled:focus,.btn-tumblr[disabled]:focus,fieldset[disabled] .btn-tumblr:focus,.btn-tumblr.disabled.focus,.btn-tumblr[disabled].focus,fieldset[disabled] .btn-tumblr.focus{background-color:#2c4762;border-color:rgba(0,0,0,0.2)} +.btn-tumblr .badge{color:#2c4762;background-color:#fff} +.btn-twitter{color:#fff;background-color:#55acee;border-color:rgba(0,0,0,0.2)}.btn-twitter:focus,.btn-twitter.focus{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)} +.btn-twitter:hover{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)} +.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active:hover,.btn-twitter.active:hover,.open>.dropdown-toggle.btn-twitter:hover,.btn-twitter:active:focus,.btn-twitter.active:focus,.open>.dropdown-toggle.btn-twitter:focus,.btn-twitter:active.focus,.btn-twitter.active.focus,.open>.dropdown-toggle.btn-twitter.focus{color:#fff;background-color:#1583d7;border-color:rgba(0,0,0,0.2)} +.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{background-image:none} +.btn-twitter.disabled:hover,.btn-twitter[disabled]:hover,fieldset[disabled] .btn-twitter:hover,.btn-twitter.disabled:focus,.btn-twitter[disabled]:focus,fieldset[disabled] .btn-twitter:focus,.btn-twitter.disabled.focus,.btn-twitter[disabled].focus,fieldset[disabled] .btn-twitter.focus{background-color:#55acee;border-color:rgba(0,0,0,0.2)} +.btn-twitter .badge{color:#55acee;background-color:#fff} +.btn-vimeo{color:#fff;background-color:#1ab7ea;border-color:rgba(0,0,0,0.2)}.btn-vimeo:focus,.btn-vimeo.focus{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)} +.btn-vimeo:hover{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)} +.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active:hover,.btn-vimeo.active:hover,.open>.dropdown-toggle.btn-vimeo:hover,.btn-vimeo:active:focus,.btn-vimeo.active:focus,.open>.dropdown-toggle.btn-vimeo:focus,.btn-vimeo:active.focus,.btn-vimeo.active.focus,.open>.dropdown-toggle.btn-vimeo.focus{color:#fff;background-color:#0f7b9f;border-color:rgba(0,0,0,0.2)} +.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{background-image:none} +.btn-vimeo.disabled:hover,.btn-vimeo[disabled]:hover,fieldset[disabled] .btn-vimeo:hover,.btn-vimeo.disabled:focus,.btn-vimeo[disabled]:focus,fieldset[disabled] .btn-vimeo:focus,.btn-vimeo.disabled.focus,.btn-vimeo[disabled].focus,fieldset[disabled] .btn-vimeo.focus{background-color:#1ab7ea;border-color:rgba(0,0,0,0.2)} +.btn-vimeo .badge{color:#1ab7ea;background-color:#fff} +.btn-vk{color:#fff;background-color:#587ea3;border-color:rgba(0,0,0,0.2)}.btn-vk:focus,.btn-vk.focus{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)} +.btn-vk:hover{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)} +.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active:hover,.btn-vk.active:hover,.open>.dropdown-toggle.btn-vk:hover,.btn-vk:active:focus,.btn-vk.active:focus,.open>.dropdown-toggle.btn-vk:focus,.btn-vk:active.focus,.btn-vk.active.focus,.open>.dropdown-toggle.btn-vk.focus{color:#fff;background-color:#3a526b;border-color:rgba(0,0,0,0.2)} +.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{background-image:none} +.btn-vk.disabled:hover,.btn-vk[disabled]:hover,fieldset[disabled] .btn-vk:hover,.btn-vk.disabled:focus,.btn-vk[disabled]:focus,fieldset[disabled] .btn-vk:focus,.btn-vk.disabled.focus,.btn-vk[disabled].focus,fieldset[disabled] .btn-vk.focus{background-color:#587ea3;border-color:rgba(0,0,0,0.2)} +.btn-vk .badge{color:#587ea3;background-color:#fff} +.btn-yahoo{color:#fff;background-color:#720e9e;border-color:rgba(0,0,0,0.2)}.btn-yahoo:focus,.btn-yahoo.focus{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)} +.btn-yahoo:hover{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)} +.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active:hover,.btn-yahoo.active:hover,.open>.dropdown-toggle.btn-yahoo:hover,.btn-yahoo:active:focus,.btn-yahoo.active:focus,.open>.dropdown-toggle.btn-yahoo:focus,.btn-yahoo:active.focus,.btn-yahoo.active.focus,.open>.dropdown-toggle.btn-yahoo.focus{color:#fff;background-color:#39074e;border-color:rgba(0,0,0,0.2)} +.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{background-image:none} +.btn-yahoo.disabled:hover,.btn-yahoo[disabled]:hover,fieldset[disabled] .btn-yahoo:hover,.btn-yahoo.disabled:focus,.btn-yahoo[disabled]:focus,fieldset[disabled] .btn-yahoo:focus,.btn-yahoo.disabled.focus,.btn-yahoo[disabled].focus,fieldset[disabled] .btn-yahoo.focus{background-color:#720e9e;border-color:rgba(0,0,0,0.2)} +.btn-yahoo .badge{color:#720e9e;background-color:#fff} \ No newline at end of file diff --git a/pydatarecognition/static/css/df_style.css b/pydatarecognition/static/css/df_style.css new file mode 100644 index 0000000..968760a --- /dev/null +++ b/pydatarecognition/static/css/df_style.css @@ -0,0 +1,20 @@ +.df_style { + font-size: 11pt; + font-family: Arial; + border-collapse: collapse; + border: 1px solid silver; + +} + +.df_style td, th { + padding: 5px; +} + +.df_style tr:nth-child(even) { + background: #E0E0E0; +} + +.df_style tr:hover { + background: silver; + cursor: pointer; +} diff --git a/pydatarecognition/static/css/dropdown_style.css b/pydatarecognition/static/css/dropdown_style.css new file mode 100644 index 0000000..7569016 --- /dev/null +++ b/pydatarecognition/static/css/dropdown_style.css @@ -0,0 +1,49 @@ +/* Style The Dropdown Button */ +.dropbtn { + background-color: black; + color: white; + padding: 16px; + font-size: 16px; + border: none; + cursor: pointer; +} + +/* The container
- needed to position the dropdown content */ +.dropdown { + position: relative; + display: inline-block; +} + +/* Dropdown Content (Hidden by Default) */ +.dropdown-content { + display: none; + position: absolute; + background-color: white; + /*min-width: 160px;*/ + width: 100%; + overflow:auto; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 1; +} + +/* Links inside the dropdown */ +.dropdown-content a { + color: black; + padding: 12px 16px; + text-decoration: none; + display: block; +} + +/* Change color of dropdown links on hover */ +.dropdown-content a:hover {background-color: grey} + +/* Show the dropdown menu on hover */ +.dropdown:hover .dropdown-content { + display: block; +} + +/* Change the background color of the dropdown button when the dropdown content is shown */ +.dropdown:hover .dropbtn { + background-color: #CCCCCC; +} + diff --git a/pydatarecognition/static/css/site_style.css b/pydatarecognition/static/css/site_style.css new file mode 100644 index 0000000..d46cb67 --- /dev/null +++ b/pydatarecognition/static/css/site_style.css @@ -0,0 +1,54 @@ +header { + background-color: #5c7e8a; + font-family: roboto, verdana, sans-serif; +} + +body { + background-color: #FAFBFC; + font-family: roboto, verdana, sans-serif; +} + +h1 { + color: #fff; + text-align: center; +} + +footer { + background-color: #5c7e8a; + font-family: roboto, verdana, sans-serif; + color: #fff; +} + +.col-container { + display: table; /* Make the container element behave like a table */ + width: 100%; /* Set full-width to expand the whole page */ +} + +.col-3 { + display: table-cell; /* Make elements inside the container behave like table cells */ + width: 30%; + max-width: 100px; + padding: 15px; + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); + transition: 0.3s; + position:relative; +} + +.col-3:hover { + box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2); +} + +.btn-col { + width: 100%; + left: 0; + bottom: 15px; + position: absolute; +} + +/* If the browser window is smaller than 600px, make the columns stack on top of each other */ +@media only screen and (max-width: 600px) { + .col-3 { + display: block; + width: 100%; + } +} diff --git a/pydatarecognition/static/img/FastAPI-logo.png b/pydatarecognition/static/img/FastAPI-logo.png new file mode 100644 index 0000000..120308c Binary files /dev/null and b/pydatarecognition/static/img/FastAPI-logo.png differ diff --git a/pydatarecognition/static/img/cif_search_logo.png b/pydatarecognition/static/img/cif_search_logo.png new file mode 100644 index 0000000..3826793 Binary files /dev/null and b/pydatarecognition/static/img/cif_search_logo.png differ diff --git a/pydatarecognition/static/img/columbia_bw.png b/pydatarecognition/static/img/columbia_bw.png new file mode 100644 index 0000000..81cb746 Binary files /dev/null and b/pydatarecognition/static/img/columbia_bw.png differ diff --git a/pydatarecognition/templates/cif_search.html b/pydatarecognition/templates/cif_search.html new file mode 100644 index 0000000..27560fd --- /dev/null +++ b/pydatarecognition/templates/cif_search.html @@ -0,0 +1,67 @@ +{% extends "cifsearch_template.html" %} + +{% block body %} + +
+ + +
+

This will accept a file containing powder data and search the database for matching literature.

+
+ +
+
+ +
+ + +
+
+ + +
+
+

Filter Query using Mongo Query Language (not required)

+ + +
+
+ + +
+
+ + +       + + +
+
+ +
+
+{% if result != None %} +
+
+ CIF Ranking +
+
+ {% autoescape false %} + {{result | replace("\n", "
")}} + {% endautoescape %} +
+
+{% endif %} +
+
+ + + + +{##} + + +{% endblock %} \ No newline at end of file diff --git a/pydatarecognition/templates/cif_search_visualization.html b/pydatarecognition/templates/cif_search_visualization.html new file mode 100644 index 0000000..0184744 --- /dev/null +++ b/pydatarecognition/templates/cif_search_visualization.html @@ -0,0 +1,51 @@ +{% extends "cifsearch_template.html" %} + +{% block body %} + + + +
+
+ NMFLogo +

nmfMapping

+
+ +
+
+

Results

+ + Top Match Comparison +

+

+ {{metadata_df_components|safe}} +

+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/pydatarecognition/templates/cifsearch_template.html b/pydatarecognition/templates/cifsearch_template.html new file mode 100644 index 0000000..db96867 --- /dev/null +++ b/pydatarecognition/templates/cifsearch_template.html @@ -0,0 +1,77 @@ + + + + CIF Search + + + + + + + + + {% block head %}{% endblock %} + + + + + + + + + +
+ + {% if user == None %} + Log In + {% else %} + + {%endif%} + +
+ +{% block body %}{% endblock %} + + + +{#
#} +{#

Powered by w3.css, Flask, and Dash

#} +{#
#} + + + + + + + \ No newline at end of file diff --git a/pydatarecognition/templates/footer-about.html b/pydatarecognition/templates/footer-about.html new file mode 100644 index 0000000..49749b9 --- /dev/null +++ b/pydatarecognition/templates/footer-about.html @@ -0,0 +1,65 @@ +{% extends "cifsearch_template.html" %} + +{% block body %} + +
+ +
+
+

About the CIF Powder Search

+

+ This is a cloud-based platform for CIF based literature search. +

+

+ This web site should help chemists, physicists, + earth scientists and anyone interested in material structure search the literature for similar data +

+

+ CIF Powder Search is free to use for academic research. However, by using, you are agreeing to the Terms of Use and Privacy Policy. +

+

+ The Billinge Group, Columbia University. +

+
+ +
+

Director

+

+ Simon J. L. Billinge +

+

+ Professor, Department of Applied Physics and Applied Mathematics,
+ Columbia University +

+

+ Physicist, Brookhaven National Laboratory +

+ +

Co-Investigators

+{#

#} +{# Matt Tucker Oak Ridge National Laboratory#} +{#

#} +{#

#} +{# Kirsten Jensen University of Copenhagen#} +{#

#} +
+ +
+

Partners and Support

+ +{# #} +{#

#} +{# This material is based upon work supported by the National Science Foundation under Grant DMREF-1534910.#} +{#

#} +{#

#} +{# Any opinions, findings, and conclusions or recommendations expressed in this material#} +{# are those of the author(s) and do not necessarily reflect the views of the National Science Foundation.#} +{#

#} + +
+ +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/pydatarecognition/templates/footer-privacy.html b/pydatarecognition/templates/footer-privacy.html new file mode 100644 index 0000000..9a6f6c6 --- /dev/null +++ b/pydatarecognition/templates/footer-privacy.html @@ -0,0 +1,18 @@ +{% extends "cifsearch_template.html" %} + +{% block body %} +
+ +
+
+

CIF Powder Search Privacy Policy

+
+
+

+ Please be very private... +{# Use of this web site is subject to the Privacy Policy described in the attached document.#} +

+
+
+
+{% endblock %} diff --git a/pydatarecognition/templates/footer-term.html b/pydatarecognition/templates/footer-term.html new file mode 100644 index 0000000..d72bf1a --- /dev/null +++ b/pydatarecognition/templates/footer-term.html @@ -0,0 +1,19 @@ +{% extends "cifsearch_template.html" %} + +{% block body %} +
+ +
+
+

CIF Powder Search Terms of Use

+
+
+

+ Please stay on good terms... +{# Use of this web site is subject to the Terms of Use described in the attached document.#} +

+ +
+
+
+{% endblock %} diff --git a/pydatarecognition/templates/landing.html b/pydatarecognition/templates/landing.html new file mode 100644 index 0000000..434b8d3 --- /dev/null +++ b/pydatarecognition/templates/landing.html @@ -0,0 +1,51 @@ +{% extends "cifsearch_template.html" %} + +{% block body %} + +{% if status == 'wronglogin' %} + +{% endif %} + +{% if flash_message %} +
    +
  • {{ flash_message }}
  • +
+{% endif %} + +
+ + +
+

Search Interface


+ cifLogo +

Search for Published Structures Similar to Experimental Data.

+
+
+
+
+ start +
+
+ +
+

FastAPI Docs


+ fastAPILogo +

Explore RestAPI Commands.

+
+
+
+
+ start +
+
+ +
+ + +
+ + +{% endblock %} + diff --git a/pydatarecognition/templates/login.html b/pydatarecognition/templates/login.html new file mode 100644 index 0000000..51aff4d --- /dev/null +++ b/pydatarecognition/templates/login.html @@ -0,0 +1,20 @@ +{% extends "cifsearch_template.html" %} + +{% block body %} + +
+
+{#

Sign in


#} +

Please sign in to access all the features of the CIF Powder Search Project. You can use one of the authentication providers as follows.

+

Please note that your account is linked to both your email and the authentication provider (e.g., Google).

+

By signing in you are agreeing to the CIF Powder Search Terms of Use and Privacy Policy.

+
+
+ + Sign in with Google + +
+
+ +{% endblock %} + diff --git a/requirements/pip_requirements.txt b/requirements/pip_requirements.txt index cf9f417..156009b 100644 --- a/requirements/pip_requirements.txt +++ b/requirements/pip_requirements.txt @@ -1 +1,3 @@ +gcloud-aio-storage +# weak point in libraries, can easily be replaced by following pydantic fastapi mongo implementation tutorials odmantic \ No newline at end of file diff --git a/requirements/run.txt b/requirements/run.txt index 40a93e2..ef4178a 100644 --- a/requirements/run.txt +++ b/requirements/run.txt @@ -17,3 +17,8 @@ uvicorn fastapi python-multipart google-cloud-storage +Authlib +httpx<0.18.2 +itsdangerous +jinja2 +aiofiles diff --git a/tests/conftest.py b/tests/conftest.py index e2550fa..c2f442d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,7 @@ from pymongo import errors as mongo_errors from xonsh.lib import subprocess from xonsh.lib.os import rmtree -from pydatarecognition.powdercif import storage, BUCKET_NAME +from pydatarecognition.powdercif import Storage, BUCKET_NAME from google.cloud.exceptions import Conflict @@ -35,7 +35,7 @@ def cif_mongodb_client(populated: bool = False) -> MongoClient: The collection will contain the test_cif_full from the inputs folder """ try: - storage_client = storage.Client() + storage_client = Storage.Client() except: print("Failed to connect to test storage bucket") yield False @@ -99,7 +99,7 @@ def cif_mongodb_client(populated: bool = False) -> MongoClient: shut_down_fork(forked, repo) if not OUTPUT_FAKE_DB: rmtree(repo) - storage_client = storage.Client() + storage_client = Storage.Client() cif_bucket = storage_client.get_bucket(BUCKET_NAME) cif_bucket.delete(force=True)