├── src ├── utils │ ├── __init__.py │ ├── metakg │ │ ├── __init__.py │ │ ├── biolink_helpers.py │ │ ├── component.py │ │ └── cytoscape_formatter.py │ ├── indices.py │ ├── http_error.py │ ├── mapping.json │ ├── decoder.py │ └── notification.py ├── handlers │ ├── __init__.py │ └── oauth.py ├── tests │ ├── _utils │ │ ├── __init__.py │ │ └── metakg │ │ │ ├── data │ │ │ ├── __init__.py │ │ │ └── litvar.json │ │ │ └── integration │ │ │ ├── __init__.py │ │ │ └── parser │ │ │ ├── __init__.py │ │ │ ├── query_operation_test.py │ │ │ ├── component_test.py │ │ │ └── index_test.py │ ├── README │ ├── config_test.py │ ├── conftest.py │ ├── decoder │ │ ├── doc_javascript.ts │ │ ├── doc_mydisease.json │ │ └── test_decoder.py │ ├── validate │ │ └── test_validate.py │ ├── format │ │ └── test_format.py │ ├── test_model.py │ └── test_query.py ├── controller │ ├── __init__.py │ ├── exceptions.py │ └── metakg.py ├── .flake8 ├── model │ ├── __init__.py │ └── base.py ├── pyproject.toml ├── schemas.ini ├── index.py ├── migrate.py └── ADMIN.md ├── .flake8 ├── web-app ├── public │ ├── favicon.ico │ ├── img │ │ ├── logos.zip │ │ ├── logo-medium.png │ │ ├── logo-small.png │ │ ├── icons │ │ │ ├── favicon.ico │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── mstile-70x70.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── mstile-144x144.png │ │ │ ├── mstile-150x150.png │ │ │ ├── mstile-310x150.png │ │ │ ├── mstile-310x310.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon-57x57.png │ │ │ ├── apple-touch-icon-72x72.png │ │ │ ├── apple-touch-icon-76x76.png │ │ │ ├── apple-touch-icon-114x114.png │ │ │ ├── apple-touch-icon-120x120.png │ │ │ ├── apple-touch-icon-144x144.png │ │ │ ├── apple-touch-icon-152x152.png │ │ │ ├── apple-touch-icon-180x180.png │ │ │ ├── browserconfig.xml │ │ │ └── safari-pinned-tab.svg │ │ ├── logo-small.svg │ │ └── logo-medium.svg │ └── manifest.json ├── src │ ├── assets │ │ └── img │ │ │ ├── mu.png │ │ │ ├── raw.gif │ │ │ ├── graph.jpg │ │ │ ├── jerry.jpg │ │ │ ├── logos.zip │ │ │ ├── lost.jpg │ │ │ ├── ncats.png │ │ │ ├── sulab.png │ │ │ ├── wulab.png │ │ │ ├── docBack.png │ │ │ ├── featured.jpg │ │ │ ├── metaex.png │ │ │ ├── mu-white.png │ │ │ ├── nih-logo.png │ │ │ ├── npmlogo.png │ │ │ ├── owl-fly.gif │ │ │ ├── scripps.png │ │ │ ├── strand.jpg │ │ │ ├── brand-hero.jpg │ │ │ ├── extensions.png │ │ │ ├── logo-large.png │ │ │ ├── logo-small.png │ │ │ ├── metakg-01.png │ │ │ ├── ncats-logo.png │ │ │ ├── Monoglyceride.ttf │ │ │ ├── graph_example.jpg │ │ │ ├── header-logo.png │ │ │ ├── logo-medium.png │ │ │ ├── logo-small-16.png │ │ │ ├── logo-small-32.png │ │ │ ├── logo-small-64.png │ │ │ ├── mstile-70x70.png │ │ │ ├── ncats-logo-1.png │ │ │ ├── openapi-logo.png │ │ │ ├── slack_default.png │ │ │ ├── swagger-logo.jpeg │ │ │ ├── user-default.png │ │ │ ├── TranslatorLogo.jpg │ │ │ ├── logo-large-text.png │ │ │ ├── logo-small-text.png │ │ │ ├── mstile-144x144.png │ │ │ ├── mstile-150x150.png │ │ │ ├── mstile-310x150.png │ │ │ ├── mstile-310x310.png │ │ │ ├── 66359F9075E37F61.jpg │ │ │ ├── 66359F9075E37F61.png │ │ │ ├── 66359F9075E37F62.jpg │ │ │ ├── 66359F9075E37F62.png │ │ │ ├── 66359F9075E37F66.jpg │ │ │ ├── apple-touch-icon.png │ │ │ ├── logo-medium-text.png │ │ │ ├── registry_plus-01.png │ │ │ ├── smart-icon-master-01.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon-57x57.png │ │ │ ├── apple-touch-icon-72x72.png │ │ │ ├── apple-touch-icon-76x76.png │ │ │ ├── apple-touch-icon-114x114.png │ │ │ ├── apple-touch-icon-120x120.png │ │ │ ├── apple-touch-icon-144x144.png │ │ │ ├── apple-touch-icon-152x152.png │ │ │ ├── apple-touch-icon-180x180.png │ │ │ ├── browserconfig.xml │ │ │ ├── site.webmanifest │ │ │ ├── lightning.svg │ │ │ ├── icon-summary.svg │ │ │ ├── ok.svg │ │ │ ├── icon-registry.svg │ │ │ ├── v0.svg │ │ │ ├── logo-small.svg │ │ │ ├── icon-metakg.svg │ │ │ ├── download.svg │ │ │ ├── v3.svg │ │ │ ├── v2.svg │ │ │ ├── logo-medium.svg │ │ │ ├── safari-pinned-tab.svg │ │ │ ├── clouds.svg │ │ │ ├── api-dryrun.svg │ │ │ ├── spec.svg │ │ │ ├── api-fail.svg │ │ │ ├── api-welcome.svg │ │ │ ├── uptodate.svg │ │ │ ├── faq.svg │ │ │ ├── api-sucess.svg │ │ │ ├── api-editor.svg │ │ │ ├── edit.svg │ │ │ └── whatis.svg │ ├── views │ │ ├── EmptyRouterView.vue │ │ ├── 404.vue │ │ ├── PortalHome.vue │ │ ├── FAQ.vue │ │ └── Editor.vue │ ├── __tests__ │ │ └── HelloWorld.spec.js │ ├── App.vue │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ ├── portals.js │ │ │ └── authentication.js │ ├── components │ │ ├── Pikaboo.vue │ │ ├── Card.vue │ │ ├── Login.vue │ │ ├── VModal.vue │ │ ├── EntityPill.vue │ │ ├── CollapsibleText.vue │ │ ├── MarkDown.vue │ │ ├── CopyButton.vue │ │ ├── MetaHead.vue │ │ ├── SimpleNetworkSigma.vue │ │ ├── SimpleNetworkCosmo.vue │ │ ├── Navigation.vue │ │ └── SimpleNetwork.vue │ ├── router │ │ └── index.js │ └── main.js ├── cypress │ ├── e2e │ │ ├── jsconfig.json │ │ └── example.cy.js │ ├── fixtures │ │ └── example.json │ └── support │ │ ├── e2e.js │ │ └── commands.js ├── .prettierrc.json ├── cypress.config.js ├── .gitignore ├── vitest.config.js ├── .eslintrc.cjs ├── README.md ├── vite.config.js └── package.json ├── docs ├── images │ ├── smartAPI-medataEditor.png │ └── smartAPI-search&browse.png ├── METAKG.md └── CREATE_API.md ├── pyproject.toml ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug-issue-report.md │ ├── report-broken.md │ └── transfer-of-ownership.md ├── workflows │ ├── check_backup.yml │ ├── app_tests.yml │ ├── deploy-prod.yml │ └── deploy-dev.yml └── scripts │ └── check_backup.py ├── requirements.txt ├── LICENSE └── README.md /src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tests/_utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/metakg/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tests/_utils/metakg/data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tests/_utils/metakg/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tests/_utils/metakg/integration/parser/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # ignore=E226,E265,E302,E402,E731,F821,W503 3 | max-line-length=160 4 | -------------------------------------------------------------------------------- /web-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/favicon.ico -------------------------------------------------------------------------------- /web-app/public/img/logos.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/logos.zip -------------------------------------------------------------------------------- /web-app/src/assets/img/mu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/mu.png -------------------------------------------------------------------------------- /web-app/src/assets/img/raw.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/raw.gif -------------------------------------------------------------------------------- /web-app/src/assets/img/graph.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/graph.jpg -------------------------------------------------------------------------------- /web-app/src/assets/img/jerry.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/jerry.jpg -------------------------------------------------------------------------------- /web-app/src/assets/img/logos.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/logos.zip -------------------------------------------------------------------------------- /web-app/src/assets/img/lost.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/lost.jpg -------------------------------------------------------------------------------- /web-app/src/assets/img/ncats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/ncats.png -------------------------------------------------------------------------------- /web-app/src/assets/img/sulab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/sulab.png -------------------------------------------------------------------------------- /web-app/src/assets/img/wulab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/wulab.png -------------------------------------------------------------------------------- /web-app/public/img/logo-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/logo-medium.png -------------------------------------------------------------------------------- /web-app/public/img/logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/logo-small.png -------------------------------------------------------------------------------- /web-app/src/assets/img/docBack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/docBack.png -------------------------------------------------------------------------------- /web-app/src/assets/img/featured.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/featured.jpg -------------------------------------------------------------------------------- /web-app/src/assets/img/metaex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/metaex.png -------------------------------------------------------------------------------- /web-app/src/assets/img/mu-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/mu-white.png -------------------------------------------------------------------------------- /web-app/src/assets/img/nih-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/nih-logo.png -------------------------------------------------------------------------------- /web-app/src/assets/img/npmlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/npmlogo.png -------------------------------------------------------------------------------- /web-app/src/assets/img/owl-fly.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/owl-fly.gif -------------------------------------------------------------------------------- /web-app/src/assets/img/scripps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/scripps.png -------------------------------------------------------------------------------- /web-app/src/assets/img/strand.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/strand.jpg -------------------------------------------------------------------------------- /docs/images/smartAPI-medataEditor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/docs/images/smartAPI-medataEditor.png -------------------------------------------------------------------------------- /web-app/public/img/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/icons/favicon.ico -------------------------------------------------------------------------------- /web-app/src/assets/img/brand-hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/brand-hero.jpg -------------------------------------------------------------------------------- /web-app/src/assets/img/extensions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/extensions.png -------------------------------------------------------------------------------- /web-app/src/assets/img/logo-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/logo-large.png -------------------------------------------------------------------------------- /web-app/src/assets/img/logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/logo-small.png -------------------------------------------------------------------------------- /web-app/src/assets/img/metakg-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/metakg-01.png -------------------------------------------------------------------------------- /web-app/src/assets/img/ncats-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/ncats-logo.png -------------------------------------------------------------------------------- /docs/images/smartAPI-search&browse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/docs/images/smartAPI-search&browse.png -------------------------------------------------------------------------------- /src/controller/__init__.py: -------------------------------------------------------------------------------- 1 | from .metakg import MetaKG 2 | from .smartapi import SmartAPI 3 | 4 | __all__ = [MetaKG, SmartAPI] 5 | -------------------------------------------------------------------------------- /web-app/src/assets/img/Monoglyceride.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/Monoglyceride.ttf -------------------------------------------------------------------------------- /web-app/src/assets/img/graph_example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/graph_example.jpg -------------------------------------------------------------------------------- /web-app/src/assets/img/header-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/header-logo.png -------------------------------------------------------------------------------- /web-app/src/assets/img/logo-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/logo-medium.png -------------------------------------------------------------------------------- /web-app/src/assets/img/logo-small-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/logo-small-16.png -------------------------------------------------------------------------------- /web-app/src/assets/img/logo-small-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/logo-small-32.png -------------------------------------------------------------------------------- /web-app/src/assets/img/logo-small-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/logo-small-64.png -------------------------------------------------------------------------------- /web-app/src/assets/img/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/mstile-70x70.png -------------------------------------------------------------------------------- /web-app/src/assets/img/ncats-logo-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/ncats-logo-1.png -------------------------------------------------------------------------------- /web-app/src/assets/img/openapi-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/openapi-logo.png -------------------------------------------------------------------------------- /web-app/src/assets/img/slack_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/slack_default.png -------------------------------------------------------------------------------- /web-app/src/assets/img/swagger-logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/swagger-logo.jpeg -------------------------------------------------------------------------------- /web-app/src/assets/img/user-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/user-default.png -------------------------------------------------------------------------------- /web-app/public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /web-app/public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /web-app/public/img/icons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/icons/mstile-70x70.png -------------------------------------------------------------------------------- /web-app/src/assets/img/TranslatorLogo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/TranslatorLogo.jpg -------------------------------------------------------------------------------- /web-app/src/assets/img/logo-large-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/logo-large-text.png -------------------------------------------------------------------------------- /web-app/src/assets/img/logo-small-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/logo-small-text.png -------------------------------------------------------------------------------- /web-app/src/assets/img/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/mstile-144x144.png -------------------------------------------------------------------------------- /web-app/src/assets/img/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/mstile-150x150.png -------------------------------------------------------------------------------- /web-app/src/assets/img/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/mstile-310x150.png -------------------------------------------------------------------------------- /web-app/src/assets/img/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/mstile-310x310.png -------------------------------------------------------------------------------- /web-app/public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /web-app/public/img/icons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/icons/mstile-144x144.png -------------------------------------------------------------------------------- /web-app/public/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /web-app/public/img/icons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/icons/mstile-310x150.png -------------------------------------------------------------------------------- /web-app/public/img/icons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/icons/mstile-310x310.png -------------------------------------------------------------------------------- /web-app/src/assets/img/66359F9075E37F61.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/66359F9075E37F61.jpg -------------------------------------------------------------------------------- /web-app/src/assets/img/66359F9075E37F61.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/66359F9075E37F61.png -------------------------------------------------------------------------------- /web-app/src/assets/img/66359F9075E37F62.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/66359F9075E37F62.jpg -------------------------------------------------------------------------------- /web-app/src/assets/img/66359F9075E37F62.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/66359F9075E37F62.png -------------------------------------------------------------------------------- /web-app/src/assets/img/66359F9075E37F66.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/66359F9075E37F66.jpg -------------------------------------------------------------------------------- /web-app/src/assets/img/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/apple-touch-icon.png -------------------------------------------------------------------------------- /web-app/src/assets/img/logo-medium-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/logo-medium-text.png -------------------------------------------------------------------------------- /web-app/src/assets/img/registry_plus-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/registry_plus-01.png -------------------------------------------------------------------------------- /web-app/src/assets/img/smart-icon-master-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/smart-icon-master-01.png -------------------------------------------------------------------------------- /web-app/src/assets/img/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/android-chrome-192x192.png -------------------------------------------------------------------------------- /web-app/src/assets/img/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/android-chrome-512x512.png -------------------------------------------------------------------------------- /web-app/src/assets/img/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /web-app/src/assets/img/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /web-app/src/assets/img/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /web-app/public/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /web-app/public/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /web-app/public/img/icons/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/icons/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /web-app/public/img/icons/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/icons/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /web-app/public/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /web-app/src/assets/img/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /web-app/src/assets/img/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /web-app/src/assets/img/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /web-app/src/assets/img/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /web-app/src/assets/img/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/src/assets/img/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /web-app/public/img/icons/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/icons/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /web-app/public/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /web-app/public/img/icons/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/icons/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /web-app/public/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /web-app/public/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartAPI/smartAPI/HEAD/web-app/public/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /src/tests/README: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | Remove entity identifiable information from test data. 4 | 5 | # WARNING 6 | 7 | Test case runs on the same index name as that of production. -------------------------------------------------------------------------------- /src/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # Let Black handle all the formatting. 3 | ignore = E,W503 4 | exclude = 5 | tests/snapshots, 6 | .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg 7 | -------------------------------------------------------------------------------- /src/model/__init__.py: -------------------------------------------------------------------------------- 1 | from .metakg import ConsolidatedMetaKGDoc, MetaKGDoc 2 | from .smartapi import SmartAPIDoc 3 | 4 | __all__ = [MetaKGDoc, SmartAPIDoc, ConsolidatedMetaKGDoc] 5 | -------------------------------------------------------------------------------- /web-app/src/views/EmptyRouterView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /web-app/cypress/e2e/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es5", "dom"], 5 | "types": ["cypress"] 6 | }, 7 | "include": ["./**/*", "../support/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /web-app/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /web-app/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": true, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "trailingComma": "none" 8 | } -------------------------------------------------------------------------------- /src/controller/exceptions.py: -------------------------------------------------------------------------------- 1 | class ControllerError(Exception): 2 | pass 3 | 4 | 5 | class NotFoundError(ControllerError): 6 | pass 7 | 8 | 9 | class ConflictError(ControllerError): 10 | pass 11 | -------------------------------------------------------------------------------- /web-app/cypress/e2e/example.cy.js: -------------------------------------------------------------------------------- 1 | // https://on.cypress.io/api 2 | 3 | describe('My First Test', () => { 4 | it('visits the app root url', () => { 5 | cy.visit('/') 6 | cy.contains('h1', 'You did it!') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | target-version = ["py38", "py39", "py310", "py311", "py312"] 4 | 5 | [tool.isort] 6 | profile = "black" 7 | combine_as_imports = true 8 | line_length = 120 9 | src_paths = ["."] 10 | -------------------------------------------------------------------------------- /src/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | target-version = ['py36', 'py37', 'py38', 'py39', 'py310'] 4 | 5 | [tool.isort] 6 | profile = "black" 7 | combine_as_imports = true 8 | line_length = 120 9 | src_paths = ["."] 10 | -------------------------------------------------------------------------------- /web-app/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}', 6 | baseUrl: 'http://localhost:4173' 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /src/tests/config_test.py: -------------------------------------------------------------------------------- 1 | from config import * 2 | 3 | ES_INDICES = { 4 | "metadata": "smartapi_docs_test", 5 | "metakg": "smartapi_metakg_docs_test", 6 | "metakg_consolidated": "smartapi_metakg_docs_consolidated_test" 7 | } 8 | 9 | COOKIE_SECRET = "test_secret" 10 | -------------------------------------------------------------------------------- /src/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | import sys 4 | 5 | SRC_PATH = pathlib.Path(__file__).parent.parent 6 | 7 | if SRC_PATH not in sys.path: 8 | sys.path.insert(0, SRC_PATH.as_posix()) 9 | 10 | if "TEST_CONF" not in os.environ: 11 | os.environ["TEST_CONF"] = "config_test.py" 12 | -------------------------------------------------------------------------------- /web-app/public/img/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #3f85bb 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /web-app/src/assets/img/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #3f85bb 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /web-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "lang": "en", 3 | "name": "SmartAPI", 4 | "short_name": "SmartAPI", 5 | "icons": [ 6 | { 7 | "src": "img/icons/apple-touch-icon-120x120.png", 8 | "sizes": "120x120", 9 | "type": "image/png" 10 | } 11 | ], 12 | "display": "fullscreen", 13 | "orientation": "landscape", 14 | "theme_color": "#328cc4", 15 | "background_color": "#328cc4" 16 | } -------------------------------------------------------------------------------- /web-app/src/__tests__/HelloWorld.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | import { mount } from '@vue/test-utils'; 4 | import HelloWorld from '../HelloWorld.vue'; 5 | 6 | describe('HelloWorld', () => { 7 | it('renders properly', () => { 8 | const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } }); 9 | expect(wrapper.text()).toContain('Hello Vitest'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /web-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /web-app/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { mergeConfig, defineConfig, configDefaults } from 'vitest/config' 3 | import viteConfig from './vite.config' 4 | 5 | export default mergeConfig( 6 | viteConfig, 7 | defineConfig({ 8 | test: { 9 | environment: 'jsdom', 10 | exclude: [...configDefaults.exclude, 'e2e/*'], 11 | root: fileURLToPath(new URL('./', import.meta.url)) 12 | } 13 | }) 14 | ) 15 | -------------------------------------------------------------------------------- /src/controller/metakg.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from collections.abc import Mapping 3 | 4 | from model import MetaKGDoc 5 | 6 | from .base import AbstractWebEntity 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class MetaKG(AbstractWebEntity, Mapping): 12 | LOOKUP_FIELDS = [] 13 | MODEL_CLASS = MetaKGDoc 14 | 15 | @classmethod 16 | def create_doc(cls, metadata): 17 | doc = MetaKGDoc(**metadata) 18 | return doc.to_dict(include_meta=True) 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore file with github OAuth IDs 2 | .ghenv 3 | /src/templates/smartapi 4 | 5 | # Python 6 | __pycache__ 7 | *.pyc 8 | 9 | # credentials 10 | src/config_key.py 11 | 12 | # backup files 13 | smartapi_*.json 14 | 15 | # allow customized biothing package 16 | /biothings 17 | /biothings.api 18 | 19 | # ignore .DS_Store on Mac 20 | .DS_Store 21 | 22 | #env 23 | /env 24 | /env-dsl 25 | .env 26 | 27 | .vscode/ 28 | *.sqlite 29 | 30 | .lock 31 | .python-version 32 | .notes 33 | -------------------------------------------------------------------------------- /web-app/src/assets/img/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SmartAPI", 3 | "short_name": "SmartAPI", 4 | "icons": [ 5 | { 6 | "src": "img/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "img/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#b2e3f5", 17 | "background_color": "#b2e3f5", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /web-app/src/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 20 | -------------------------------------------------------------------------------- /web-app/src/assets/img/lightning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /web-app/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-prettier/skip-formatting' 10 | ], 11 | overrides: [ 12 | { 13 | files: [ 14 | 'cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}' 15 | ], 16 | 'extends': [ 17 | 'plugin:cypress/recommended' 18 | ] 19 | } 20 | ], 21 | parserOptions: { 22 | ecmaVersion: 'latest' 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /web-app/src/views/404.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /src/tests/decoder/doc_javascript.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "API Document", 6 | "description": "API Description", 7 | "termsOfService": null, 8 | "contact": { 9 | "name": "John Doe", 10 | "email": "jdoe@example.com", 11 | "url": "https://example.com", 12 | }, 13 | "license": { 14 | "name": "Apache 2.0", 15 | "url": "https://www.apache.org/licenses/LICENSE-2.0.html", 16 | }, 17 | }, 18 | "paths": {} 19 | } -------------------------------------------------------------------------------- /web-app/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex'; 2 | import { authentication } from './modules/authentication'; 3 | import { metakg } from './modules/metakg'; 4 | import { faq } from './modules/faq'; 5 | import { about } from './modules/about'; 6 | import { portals } from './modules/portals'; 7 | import { registry } from './modules/registry'; 8 | import { extensions } from './modules/extensions'; 9 | 10 | export default createStore({ 11 | modules: { 12 | authentication, 13 | metakg, 14 | faq, 15 | about, 16 | portals, 17 | registry, 18 | extensions 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /web-app/src/components/Pikaboo.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | 15 | 32 | -------------------------------------------------------------------------------- /docs/METAKG.md: -------------------------------------------------------------------------------- 1 | ##### What is Meta-KG? 2 | 3 | The SmartAPI Meta Knowledge Graph (Meta-KG) represents how biomedical concepts can be connected through APIs. Each node in the meta-KG represents a biolink entity type, e.g. Gene, SequenceVariant, ChemicalSubstance. Each edge in the meta-KG represents a unique combination of a biolink predicate, e.g. targets, treats, and an API which delivers the association. 4 | 5 | The Meta-KG is constructed using the collection of SmartAPI specifications currently registered in SmartAPI. All APIs registered with x-smartapi field will be integrated into the meta-KG. In addition, all ReasonerStdAPIs registered in SmartAPI and implemented the **/predicate** endpoint will also be integrated. 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/schemas.ini: -------------------------------------------------------------------------------- 1 | [swagger] 2 | swagger_v2: https://raw.githubusercontent.com/swagger-api/swagger-editor/v3.6.1/src/plugins/validate-json-schema/structural-validation/swagger2-schema.js 3 | 4 | [openapi] 5 | openapi_v3.0: https://raw.githubusercontent.com/swagger-api/swagger-editor/v3.15.6/src/plugins/json-schema-validator/oas3-schema.yaml 6 | openapi_v3.1: https://raw.githubusercontent.com/swagger-api/validator-badge/v2.1.5/src/main/resources/schemas/31/official.json 7 | x-translator: https://raw.githubusercontent.com/NCATSTranslator/translator_extensions/production/x-translator/smartapi_x-translator_schema.json 8 | x-trapi: https://raw.githubusercontent.com/NCATSTranslator/translator_extensions/production/x-trapi/smartapi_x-trapi_schema.json 9 | -------------------------------------------------------------------------------- /web-app/src/store/modules/portals.js: -------------------------------------------------------------------------------- 1 | import translator_img from '@/assets/img/TranslatorLogo.jpg'; 2 | 3 | export const portals = { 4 | state: () => ({ 5 | portals: [ 6 | { 7 | name: 'translator', 8 | title: 'Translator', 9 | link: '/portal/translator', 10 | image: translator_img, 11 | description: 12 | 'This program focuses on building tools for massive knowledge integration in support of biomedical and translational science. Learn more' 13 | } 14 | ] 15 | }), 16 | strict: true, 17 | getters: { 18 | portals: (state) => { 19 | return state.portals; 20 | } 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-issue-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug/Issue Report 3 | about: Report an issue with SmartAPI 4 | title: '' 5 | labels: bug 6 | assignees: marcodarko 7 | 8 | --- 9 | 10 | # Expected Behavior 11 | 12 | Please describe the behavior you are expecting 13 | 14 | # Current Behavior 15 | 16 | What is the current behavior? 17 | 18 | # Failure Information (for bugs) 19 | 20 | Please help provide information about the failure if this is a bug. If it is not a bug, please remove the rest of this template. 21 | 22 | ## Steps to Reproduce 23 | 24 | Please provide detailed steps for reproducing the issue. 25 | 26 | 1. step 1 27 | 2. step 2 28 | 3. you get it... 29 | 30 | ## Failure Logs 31 | 32 | Please include any relevant log snippets or files here. 33 | -------------------------------------------------------------------------------- /web-app/cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /src/tests/_utils/metakg/integration/parser/query_operation_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from utils.metakg.query_operation import QueryOperationObject 4 | 5 | 6 | class TestQueryOperationObjectClass(unittest.TestCase): 7 | def test_missing_fiels_should_return_none(self): 8 | op = { 9 | "parameters": {"gene": "{inputs[0]}"}, 10 | "requestBody": {id: "{inputs[1]"}, 11 | "supportBatch": "false", 12 | "inputs": [{id: "NCBIGene", "semantic": "Gene"}], 13 | "outputs": [{"id": "NCBIGene", "semantic": "Gene"}], 14 | "predicate": "related_to", 15 | "response_mapping": {}, 16 | } 17 | 18 | obj = QueryOperationObject() 19 | obj.xBTEKGSOperation = op 20 | self.assertIsNone(obj.input_separator) 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | biothings[web_extra]==1.0.1 2 | # git+https://github.com/biothings/biothings.api.git@1.0.x#egg=biothings[web_extra] 3 | 4 | # document validation 5 | jsonschema>=4.4.0 6 | 7 | # web handling 8 | pycurl==7.45.2 # to use curl_httpclient in tornado 9 | #chardet==3.0.4 10 | aiocron==1.8 11 | 12 | # gitdb version specified because gitdb.utils.compat not available in newest version 13 | gitdb==4.0.11 14 | 15 | # local issuer certificate 16 | certifi 17 | 18 | # pytest 19 | 20 | # used in admin.py to lock file 21 | filelock 22 | # used in admin.py to upload file to s3 23 | boto3 24 | 25 | # Biolink Model Toolkit, used in /api/metakg endpoint 26 | bmt-lite-v3.1.0==2.2.2; python_version >= "3.7.0" and python_version < "3.9.0" 27 | bmt-lite-v3.6.0==2.3.0; python_version >= "3.9.0" 28 | networkx==3.1.0 29 | -------------------------------------------------------------------------------- /src/utils/metakg/biolink_helpers.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union 2 | 3 | import bmt 4 | 5 | # Initialize the Biolink Model Toolkit instance globally if it's used frequently 6 | # or pass it as a parameter to functions that require it. 7 | toolkit = bmt.Toolkit() 8 | 9 | 10 | def get_expanded_values(value: Union[str, List[str]], toolkit_instance=toolkit) -> List[str]: 11 | """Return expanded value list for a given Biolink class name.""" 12 | if isinstance(value, str): 13 | value = [value] 14 | _out = [] 15 | for v in value: 16 | try: 17 | v = toolkit_instance.get_descendants(v, reflexive=True, formatted=True) 18 | v = [x.split(":")[-1] for x in v] # Remove 'biolink:' prefix 19 | except ValueError: 20 | v = [v] 21 | _out.extend(v) 22 | return _out 23 | -------------------------------------------------------------------------------- /src/utils/metakg/component.py: -------------------------------------------------------------------------------- 1 | class Components: 2 | components = {} 3 | 4 | def __init__(self, components): 5 | self.components = components 6 | 7 | def fetch(self, obj, key): 8 | if key in ["#", "components"]: 9 | return obj 10 | if obj and key in obj: 11 | return obj[key] 12 | return None 13 | 14 | def fetch_component_by_ref(self, ref): 15 | if ref.startswith("#/components/"): 16 | if ref[-1] == "/": 17 | ref = ref[0:-1] 18 | res = self.components 19 | paths = ref.split("/") 20 | try: 21 | for ele in paths: 22 | res = self.fetch(res, ele) 23 | except Exception: 24 | return None 25 | return res 26 | return None 27 | -------------------------------------------------------------------------------- /web-app/src/assets/img/icon-summary.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /web-app/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /web-app/src/assets/img/ok.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /web-app/src/assets/img/icon-registry.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 12 | 14 | 16 | 17 | -------------------------------------------------------------------------------- /web-app/src/views/PortalHome.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/report-broken.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report Broken/Down API 3 | about: Report a broken API or an issue with any API 4 | title: '' 5 | labels: help wanted 6 | assignees: marcodarko 7 | 8 | --- 9 | 10 | ## Report a broken API 11 | 12 | Fill this out if you would like to report an issue with an API. Please include as much detail as possible to make sure we address your issue as soon as possible. 13 | 14 | 15 | 16 | *Let's get some information first* 17 | 18 | **I'm am reporting...** 19 | 20 | - [ ] An API with broken source URL 21 | 22 | - [ ] An API Down 23 | 24 | - [ ] An API with FAIL uptime 25 | 26 | - [ ] A Different issue. Please specify below. 27 | 28 | 29 | * Details * 30 | 31 | 32 | * What is the ***name*** and ***id*** of the API? (You can find the ID under the Details section of the API) *eg. TheAPI : 124094327094378* 33 | 34 | 35 | 36 | 37 | * How is this issue affecting your work? This can help us prioritize our issues. 38 | 39 | 40 | 41 | 42 | Thank you! We will review this request ASAP and will inform you of any progress. 43 | -------------------------------------------------------------------------------- /web-app/src/components/Card.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 46 | -------------------------------------------------------------------------------- /web-app/src/assets/img/v0.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 The Network of BioThings Consortium 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/check_backup.yml: -------------------------------------------------------------------------------- 1 | name: Check S3 Backup and Notify Slack 2 | 3 | on: 4 | workflow_dispatch: # Allows manual trigger from GitHub Actions UI 5 | schedule: 6 | - cron: '0 13 * * *' # 5:00 AM PST (UTC-8) 7 | 8 | jobs: 9 | check-backup: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v2 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: '3.x' 20 | 21 | - name: Install boto3 (AWS SDK for Python) 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install boto3 requests 25 | 26 | - name: Check if backup exists in S3 27 | run: python .github/scripts/check_backup.py 28 | env: 29 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 30 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 31 | AWS_REGION: ${{ secrets.AWS_REGION }} 32 | BACKUP_BUCKET_NAME: "${{ secrets.BACKUP_BUCKET_NAME }}" 33 | S3_FOLDER: "db_backup/" 34 | SLACK_CHANNEL: "#ncats-translator" 35 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 36 | -------------------------------------------------------------------------------- /web-app/public/img/logo-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /web-app/src/assets/img/logo-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/utils/indices.py: -------------------------------------------------------------------------------- 1 | from elasticsearch_dsl import Index 2 | from model import SmartAPIDoc 3 | 4 | 5 | def exists(model_class=SmartAPIDoc, index=None): 6 | return Index(index or model_class.Index.name).exists() 7 | 8 | 9 | def setup(model_class=SmartAPIDoc, index=None): 10 | """ 11 | Setup Elasticsearch Index with dynamic template. 12 | Run it on an open index to update dynamic mapping. 13 | """ 14 | 15 | if not exists(model_class, index=index): 16 | model_class.init(index=index) 17 | 18 | # if model_class == SmartAPIDoc: 19 | # # set up custom mapping for SmartAPI index 20 | # _dirname = os.path.dirname(__file__) 21 | # with open(os.path.join(_dirname, "mapping.json"), "r") as file: 22 | # mapping = json.load(file) 23 | # Index(model_class.Index.name).put_mapping(body=mapping) 24 | 25 | 26 | def delete(model_class=SmartAPIDoc, index=None): 27 | Index(index or model_class.Index.name).delete() 28 | 29 | 30 | def reset(model_class=SmartAPIDoc, index=None): 31 | if exists(model_class, index=index): 32 | delete(model_class, index=index) 33 | 34 | setup(model_class, index=index) 35 | 36 | 37 | def refresh(model_class=SmartAPIDoc, index=None): 38 | idx = Index(index or model_class.Index.name) 39 | idx.refresh() 40 | -------------------------------------------------------------------------------- /web-app/src/assets/img/icon-metakg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/tests/_utils/metakg/integration/parser/component_test.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import unittest 4 | 5 | from utils.metakg.component import Components 6 | 7 | 8 | class TestComponent(unittest.TestCase): 9 | def test_ref_with_trailing_slash(self): 10 | with open(os.path.abspath(os.path.join(os.path.dirname(__file__), "components.json"))) as f: 11 | components = json.load(f) 12 | cp_obj = Components(components) 13 | rec = cp_obj.fetch_component_by_ref("#/components/x-bte-kgs-operations/enablesMF/") 14 | self.assertEqual(rec[0]["source"], "entrez") 15 | 16 | def test_wrong_ref(self): 17 | with open(os.path.abspath(os.path.join(os.path.dirname(__file__), "components.json"))) as f: 18 | components = json.load(f) 19 | cp_obj = Components(components) 20 | rec = cp_obj.fetch_component_by_ref("/components/x-bte-response-mapping") 21 | self.assertEqual(rec, None) 22 | 23 | def test_wrong_ref_2(self): 24 | with open(os.path.abspath(os.path.join(os.path.dirname(__file__), "components.json"))) as f: 25 | components = json.load(f) 26 | cp_obj = Components(components) 27 | rec = cp_obj.fetch_component_by_ref("#/components/x-bte-response-mapping/hello/world") 28 | self.assertEqual(rec, None) 29 | -------------------------------------------------------------------------------- /web-app/README.md: -------------------------------------------------------------------------------- 1 | # SmartAPI web app 2 | 3 | Developed with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 8 | 9 | ## Customize configuration 10 | 11 | See [Vite Configuration Reference](https://vitejs.dev/config/). 12 | 13 | ## Project Setup 14 | 15 | ```sh 16 | npm install 17 | ``` 18 | 19 | ### Compile and Hot-Reload for Development 20 | 21 | ```sh 22 | npm run dev 23 | ``` 24 | 25 | ### Compile and Minify for Production 26 | 27 | ```sh 28 | npm run build 29 | ``` 30 | 31 | ### Run Unit Tests with [Vitest](https://vitest.dev/) 32 | 33 | ```sh 34 | npm run test:unit 35 | ``` 36 | 37 | ### Run End-to-End Tests with [Cypress](https://www.cypress.io/) 38 | 39 | ```sh 40 | npm run test:e2e:dev 41 | ``` 42 | 43 | This runs the end-to-end tests against the Vite development server. 44 | It is much faster than the production build. 45 | 46 | But it's still recommended to test the production build with `test:e2e` before deploying (e.g. in CI environments): 47 | 48 | ```sh 49 | npm run build 50 | npm run test:e2e 51 | ``` 52 | 53 | ### Lint with [ESLint](https://eslint.org/) 54 | 55 | ```sh 56 | npm run lint 57 | ``` 58 | -------------------------------------------------------------------------------- /web-app/src/store/modules/authentication.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const authentication = { 4 | state: () => ({ 5 | userInfo: {}, 6 | loggedIn: false 7 | }), 8 | mutations: { 9 | saveUser(state, payload) { 10 | state.userInfo = payload['user']; 11 | state.loggedIn = Object.prototype.hasOwnProperty.call(state.userInfo, 'login') ? true : false; 12 | }, 13 | resetUser(state) { 14 | state.userInfo = {}; 15 | state.loggedIn = false; 16 | } 17 | }, 18 | actions: { 19 | checkUser({ commit }) { 20 | if (process.env.NODE_ENV == 'development') { 21 | // for dev only 22 | commit('saveUser', { 23 | user: { 24 | name: 'Marco Cano', 25 | email: 'artofmarco@gmail.com', 26 | login: 'marcodarko', 27 | avatar_url: 'https://avatars.githubusercontent.com/u/23092057?v=4' 28 | } 29 | }); 30 | } else { 31 | axios 32 | .get('/user') 33 | .then((response) => { 34 | commit('saveUser', { user: response.data }); 35 | }) 36 | .catch((err) => { 37 | commit('resetUser'); 38 | throw err; 39 | }); 40 | } 41 | } 42 | }, 43 | getters: { 44 | userInfo: (state) => { 45 | return state.userInfo; 46 | }, 47 | loggedIn: (state) => { 48 | return state.loggedIn; 49 | } 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/transfer-of-ownership.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Transfer of Ownership 3 | about: Transfer a registration to another person 4 | title: '' 5 | labels: help wanted 6 | assignees: marcodarko 7 | 8 | --- 9 | 10 | ## Transfer of Ownership 11 | 12 | Fill this out if you would like to transfer ownership of an API entry to someone else or claim ownership of one. 13 | 14 | **Note: This transfer of ownership requests can only come from a registered user and go to another registered user on SmartAPI**. 15 | 16 | 17 | 18 | 19 | *Let's start! First, tell us who you are* 20 | 21 | * **I'm am ...** 22 | 23 | - [ ] the person transferring ownership of my API 24 | 25 | - [ ] the person that this API is being transferred to 26 | 27 | - [ ] a third party requesting a special transfer (please provide details) 28 | 29 | 30 | 31 | * What is the ***name*** and ***id*** of the API you would like to transfer? *eg. MyAPI : 124094327094378* 32 | 33 | 34 | 35 | 36 | * What is the ***name*** and ***username*** of the person that this API currently belongs to? *eg. Jane Doe @janedoe1 37 | 38 | 39 | 40 | 41 | * What is the ***name*** and ***username*** of the person that this API is being transferred to? *eg. John Doe @johndoe1* 42 | 43 | 44 | 45 | 46 | * What is the reason this transfer of ownership is being requested? 47 | 48 | 49 | 50 | 51 | Thank you! We will review this request ASAP and will inform you of the status of the transfer. 52 | -------------------------------------------------------------------------------- /web-app/src/components/Login.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 36 | 37 | 62 | -------------------------------------------------------------------------------- /.github/workflows/app_tests.yml: -------------------------------------------------------------------------------- 1 | name: App Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | run_app_tests: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 18 | steps: 19 | - name: Checkout source 20 | uses: actions/checkout@v4 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Update 26 | run: sudo apt-get update 27 | - name: Install apt-get packages 28 | run: sudo apt-get install libssl-dev libcurl4-openssl-dev 29 | - name: Upgrade pip 30 | run: pip install --upgrade pip 31 | - name: Install dependencies 32 | run: pip install -r requirements.txt 33 | - name: Install PyTest 34 | run: pip install pytest 35 | - name: Run App Tests 36 | run: pytest tests 37 | working-directory: src 38 | services: 39 | Elasticsearch: 40 | image: docker.elastic.co/elasticsearch/elasticsearch:8.17.0 41 | env: 42 | "discovery.type": single-node 43 | "xpack.security.enabled": false 44 | "xpack.security.http.ssl.enabled": false 45 | "xpack.security.transport.ssl.enabled": false 46 | ports: 47 | - 9200:9200 48 | -------------------------------------------------------------------------------- /web-app/src/components/VModal.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 24 | 25 | 65 | -------------------------------------------------------------------------------- /src/utils/http_error.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Any 2 | from tornado.web import HTTPError 3 | import re 4 | 5 | 6 | class SmartAPIHTTPError(HTTPError): 7 | """An extended HTTPError class with additional details and message sanitization. 8 | 9 | Adds the following enhancements: 10 | - A `details` parameter for including extra context about the error. 11 | - A `clean_error_message` method for sanitizing log messages and details. 12 | 13 | :arg str details: Additional information about the error. 14 | """ 15 | 16 | def __init__( 17 | self, 18 | status_code: int = 500, 19 | log_message: Optional[str] = None, 20 | *args: Any, 21 | **kwargs: Any, 22 | ) -> None: 23 | super().__init__(status_code, log_message, *args, **kwargs) 24 | if self.reason: 25 | self.reason = self.clean_error_message(self.reason) 26 | if self.log_message: 27 | self.log_message = self.clean_error_message(self.log_message) 28 | 29 | @staticmethod 30 | def clean_error_message(message: str) -> str: 31 | """ 32 | Sanitizes an error message by replacing newlines, tabs, and reducing multiple spaces. 33 | 34 | :param message: The error message to sanitize. 35 | :return: A cleaned and sanitized version of the message. 36 | """ 37 | message = message.replace("\n", " ") # Replace actual newlines with spaces 38 | message = re.sub(r'\s+', ' ', message) # Normalize spaces 39 | return message.strip() 40 | -------------------------------------------------------------------------------- /web-app/src/components/EntityPill.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 56 | -------------------------------------------------------------------------------- /web-app/src/assets/img/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /web-app/src/components/CollapsibleText.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 65 | -------------------------------------------------------------------------------- /web-app/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router'; 2 | import axios from 'axios'; 3 | import { routes } from './routes.js'; 4 | 5 | const router = createRouter({ 6 | history: createWebHistory(import.meta.env.BASE_URL), 7 | routes, 8 | linkActiveClass: 'route-active', 9 | scrollBehavior() { 10 | return { top: 0 }; 11 | } 12 | }); 13 | 14 | router.beforeEach((to, from, next) => { 15 | if (to.name === 'Home') { 16 | if (window.location.hostname == 't.biothings.io' || window.location.host == 't.biothings.io') { 17 | next({ name: 'UI', params: { smartapi_id: 'f7943e6167166b3ea9e4b8be08f45fa6' } }); 18 | } else { 19 | const slug = window.location.host.split('.')[0]; 20 | 21 | if (!['www', 'dev', 'smart-api', 'localhost:8000', 'localhost:8080'].includes(slug)) { 22 | axios 23 | .get('https://smart-api.info/api/metadata/' + slug + '?fields=_id&raw=1') 24 | .then((res) => { 25 | if (Object.prototype.hasOwnProperty.call(res.data, '_id')) { 26 | next({ name: 'UI', params: { smartapi_id: res.data._id } }); 27 | try { 28 | //hack to change url back to home 29 | history.pushState({}, '', '/'); 30 | } catch (e) { 31 | console.log(`unable to change url because ${e}`); 32 | } 33 | } else next(); 34 | }) 35 | .catch((err) => { 36 | next(); 37 | throw err; 38 | }); 39 | } else next(); 40 | } 41 | } else next(); 42 | }); 43 | 44 | export default router; 45 | -------------------------------------------------------------------------------- /web-app/src/assets/img/v3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 15 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /web-app/src/assets/img/v2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | 17 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /web-app/public/img/logo-medium.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /web-app/src/components/MarkDown.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 67 | -------------------------------------------------------------------------------- /web-app/src/assets/img/logo-medium.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/model/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Elasticsearch Document Object Base Model 3 | """ 4 | from config import ES_HOST 5 | from elasticsearch_dsl import A, Document, MetaField, connections 6 | 7 | # create a default connection 8 | connections.create_connection(hosts=ES_HOST) 9 | 10 | 11 | class BaseDoc(Document): 12 | class Meta: 13 | """ 14 | Index Mappings 15 | """ 16 | 17 | dynamic = MetaField(True) 18 | abstract = True 19 | 20 | @classmethod 21 | def exists(cls, value, field="_id", index=None): 22 | """ 23 | Return the first matching document's _id or None. 24 | Data could change after query, use try-catch for 25 | any follow up operations like Document.get(_id). 26 | """ 27 | search = cls.search(index=index).query("match", **{field: value}) 28 | if search.count(): 29 | return next(iter(search)).meta.id 30 | return None 31 | 32 | @classmethod 33 | def aggregate(cls, field="tags.name", index=None): 34 | """ 35 | Perform terms aggregation on a keyword field. 36 | Add multi-field keyword indexing suffix automatically. 37 | """ 38 | 39 | if not field.endswith(".raw") and not field.startswith("_"): 40 | field = field + ".raw" # so that it's a keyword field 41 | 42 | # build the aggregation query 43 | agg = A("terms", field=field, size=25) 44 | search = cls.search(index=index) 45 | search.aggs.bucket("aggs", agg) 46 | 47 | # transform the response to a simpler format 48 | buckets = search.execute().aggregations.aggs.buckets 49 | result = {b["key"]: b["doc_count"] for b in buckets} 50 | 51 | return result 52 | 53 | def get_url(self): 54 | raise NotImplementedError() 55 | -------------------------------------------------------------------------------- /src/index.py: -------------------------------------------------------------------------------- 1 | """ SmartAPI Entry Point """ 2 | 3 | import logging 4 | from os.path import exists 5 | 6 | from threading import Thread 7 | 8 | from aiocron import crontab 9 | from biothings.web.launcher import main 10 | from tornado.ioloop import IOLoop 11 | from tornado.web import RequestHandler 12 | from tornado.options import define, options 13 | 14 | from admin import routine 15 | from utils.indices import setup 16 | 17 | define("prod", default=False, help="Run in production mode", type=bool) 18 | 19 | 20 | def run_routine(): 21 | thread = Thread(target=routine, daemon=True) 22 | thread.start() 23 | 24 | 25 | class WebAppHandler(RequestHandler): 26 | def get(self): 27 | if exists("../web-app/dist/index.html"): 28 | self.render("../web-app/dist/index.html") 29 | 30 | 31 | if __name__ == "__main__": 32 | logger = logging.getLogger("routine") 33 | options.parse_command_line() 34 | if not options.debug and options.prod: 35 | crontab("0 0 * * *", func=run_routine, start=True) 36 | logger.info("Crontab configured successfully.") 37 | 38 | IOLoop.current().add_callback(setup) 39 | main( 40 | [ 41 | (r"/user/?", "handlers.api.UserInfoHandler"), 42 | (r"/login/?", "handlers.api.LoginHandler"), 43 | (r"/oauth", "handlers.oauth.GitHubLoginHandler"), 44 | (r"/logout/?", "handlers.api.LogoutHandler"), 45 | (r"/sitemap.xml()", "tornado.web.StaticFileHandler", {"path": "../web-app/dist/sitemap.xml"}), 46 | (r"/((?:img|assets)/.*)", "tornado.web.StaticFileHandler", {"path": "../web-app/dist/"}), 47 | ], 48 | { 49 | "default_handler_class": WebAppHandler, 50 | "static_path": "../web-app/dist/", 51 | }, 52 | use_curl=True, 53 | ) 54 | -------------------------------------------------------------------------------- /web-app/src/main.js: -------------------------------------------------------------------------------- 1 | // Components 2 | import App from '@/App.vue'; 3 | import VModal from '@/components/VModal.vue'; 4 | import MetaHead from '@/components/MetaHead.vue'; 5 | import CopyButton from '@/components/CopyButton.vue'; 6 | // Companion Libraries 7 | import router from '@/router'; 8 | import store from '@/store'; 9 | // Plugins 10 | import VueSweetalert2 from 'vue-sweetalert2'; 11 | import VueFinalModal from 'vue-final-modal'; 12 | import Particles from 'vue3-particles'; 13 | import VueGtag from 'vue-gtag-next'; 14 | import Toaster from '@meforma/vue-toaster'; 15 | import { delegate } from 'tippy.js'; 16 | // Global CSS 17 | import 'sweetalert2/dist/sweetalert2.min.css'; 18 | import 'materialize-css/dist/css/materialize.css'; 19 | import 'material-design-icons/iconfont/material-icons.css'; 20 | import '@/assets/app.css'; 21 | import '@/assets/animista.css'; 22 | import 'tippy.js/themes/light.css'; 23 | 24 | import { createApp } from 'vue'; 25 | 26 | const app = createApp(App); 27 | 28 | app 29 | .use(store) 30 | .use(router) 31 | .use(VueSweetalert2) 32 | .use(VueFinalModal()) 33 | .use(Particles) 34 | .use(Toaster) 35 | .use(VueGtag, { 36 | property: { 37 | id: 'UA-139873613-1' 38 | } 39 | }); 40 | // dev base api url 41 | app.config.globalProperties.$apiUrl = 42 | process.env.NODE_ENV == 'development' ? 'http://localhost:8000/api' : '/api'; 43 | 44 | // global registration 45 | app.component('VModal', VModal); 46 | app.component('MetaHead', MetaHead); 47 | app.component('CopyButton', CopyButton); 48 | app.mount('#app'); 49 | 50 | delegate('#app', { 51 | target: '[data-tippy-content]', 52 | theme: 'light', 53 | trigger: 'mouseenter', 54 | interactive: true, 55 | allowHTML: true, 56 | onShow(instance) { 57 | instance.setContent(instance.reference.dataset.tippyContent); 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /.github/workflows/deploy-prod.yml: -------------------------------------------------------------------------------- 1 | name: Deploy-to-EC2-Prod 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | deploy: 7 | name: Deploy to EC2 manually 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Step 0 - Install APT Dependences 12 | run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev libssl-dev libxml2-dev libxmlsec1-dev libxmlsec1-openssl libxml2 libxmlsec1 pkg-config 13 | 14 | - name: Step 1 - Checkout the Files 15 | uses: actions/checkout@v3 16 | 17 | - name: Step 2 - Install Node.js 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: '20.9.0' 21 | 22 | - name: Step 3 - Build Frontend 23 | run: | 24 | cd web-app 25 | npm install 26 | NODE_OPTIONS="--max-old-space-size=1512" npm run build 27 | 28 | - name: Step 4 - Deploy to Prod Server 29 | uses: easingthemes/ssh-deploy@main 30 | env: 31 | SOURCE: "/" 32 | SSH_PRIVATE_KEY: ${{ secrets.AWS_PROD_EC2_SSH_KEY }} 33 | REMOTE_HOST: ${{ secrets.AWS_PROD_HOST_DNS }} 34 | REMOTE_USER: ${{ secrets.AWS_PROD_USERNAME }} 35 | TARGET: ${{ secrets.AWS_PROD_TARGET_DIR }} 36 | SCRIPT_AFTER: | 37 | set -x 38 | echo "Activate python env." 39 | cd /home/ubuntu/smartapi 40 | source .env/bin/activate 41 | echo "Installing backend requirements." 42 | pip install --upgrade pip 43 | pip install -Ur requirements.txt --no-cache-dir --ignore-installed --force-reinstall 44 | echo "Restarting smartapi backend services..." 45 | sudo systemctl restart smartapi@8000 46 | sleep 10 47 | sudo systemctl restart smartapi@8080 48 | echo "Smartapi backend services restarted!!!" 49 | set +x 50 | echo $RSYNC_STDOUT 51 | -------------------------------------------------------------------------------- /web-app/vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | import { defineConfig } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | import Sitemap from 'vite-plugin-sitemap' 5 | import axios from 'axios' 6 | 7 | // Static routes 8 | const staticRoutes = [ 9 | '/', 10 | '/about', 11 | '/faq', 12 | '/privacy', 13 | '/branding', 14 | '/guide', 15 | '/extensions/smartapi', 16 | '/extensions/x-bte', 17 | '/extensions/x-translator?', 18 | '/registry', 19 | '/registry/translator', 20 | '/documentation/getting-started', 21 | '/documentation/smartapi-extensions', 22 | '/documentation/openapi-specification', 23 | '/ui', // For routes like /ui/:smartapi_id 24 | '/editor', 25 | ] 26 | 27 | // Fetch dynamic slugs asynchronously 28 | async function fetchDynamicRoutes() { 29 | try { 30 | const response = await axios.get('https://smart-api.info/api/query?&q=__all__&fields=_id&size=1000&raw=1') 31 | const dynamicRoutes = response.data.hits.map(item => `/ui/${item._id}`) // Only return URLs as strings 32 | console.log('Fetched dynamic routes:', dynamicRoutes) // Log the dynamic routes 33 | return dynamicRoutes 34 | } catch (error) { 35 | console.error('Error fetching dynamic routes:', error) 36 | return [] // Return an empty array in case of error 37 | } 38 | } 39 | 40 | // Fetch dynamic routes before config execution 41 | const routes = await fetchDynamicRoutes() 42 | 43 | // Combine static and dynamic routes 44 | const dynamicRoutes = [...staticRoutes, ...routes] 45 | 46 | export default defineConfig({ 47 | plugins: [ 48 | vue(), 49 | Sitemap({ 50 | hostname: 'https://smart-api.info', 51 | readable: true, 52 | changefreq: 'monthly', 53 | dynamicRoutes // Provide the combined routes as an array of strings 54 | }) 55 | ], 56 | resolve: { 57 | alias: { 58 | '@': fileURLToPath(new URL('./src', import.meta.url)) 59 | } 60 | } 61 | }) 62 | -------------------------------------------------------------------------------- /src/utils/metakg/cytoscape_formatter.py: -------------------------------------------------------------------------------- 1 | """ 2 | Transform ES hits to a cytoscape-ready data format used to render a network graph with &format=html 3 | 4 | """ 5 | 6 | 7 | class CytoscapeDataFormatter(): 8 | """ 9 | Accepts a chunk of ES results and returns them cytoscape compatible format 10 | that contains all available Nodes and Edges. 11 | Node: 12 | { 13 | 'data': { 'id': 'A', 'weight': 1, 'label': 'EntityName' } 14 | } 15 | Edge: 16 | { 17 | 'data': { 'id': 'AB', 'source': 'A', 'target': 'B', 'apis': '[]' } 18 | } 19 | 20 | """ 21 | 22 | def __init__(self, chunk): 23 | self.hits = chunk 24 | self.node_ids = [] 25 | self.nodes = [] 26 | self.edges = [] 27 | 28 | def add_node(self, entity_name): 29 | if entity_name not in self.node_ids: 30 | self.node_ids.append(entity_name) 31 | self.nodes.append({ 32 | 'group': 'nodes', 33 | 'data': { 34 | 'id': entity_name, 35 | 'weight': 1, 36 | 'label': entity_name, 37 | 'colors': "#df4bfc #4a148c" 38 | } 39 | }) 40 | 41 | def add_edge(self, sub, obj, predicate, apis): 42 | self.edges.append({ 43 | 'group': 'edges', 44 | 'data': { 45 | 'id': predicate + sub + obj, 46 | 'source': sub, 'target': obj, 47 | 'predicate': predicate, 48 | 'apis': apis, 49 | 'color': 'black' 50 | } 51 | }) 52 | 53 | def get_data(self): 54 | for edge in self.hits: 55 | self.add_node(edge['subject']) 56 | self.add_node(edge['object']) 57 | self.add_edge(edge['subject'], edge['object'], edge['predicate'], edge['api']) 58 | return self.nodes + self.edges 59 | -------------------------------------------------------------------------------- /.github/workflows/deploy-dev.yml: -------------------------------------------------------------------------------- 1 | name: Deploy-to-EC2-Dev 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | deploy: 11 | name: Deploy to EC2 on branch push 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Step 0 - Install APT Dependences 16 | run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev libssl-dev libxml2-dev libxmlsec1-dev libxmlsec1-openssl libxml2 libxmlsec1 pkg-config 17 | 18 | - name: Step 1 - Checkout the Files 19 | uses: actions/checkout@v3 20 | 21 | - name: Step 2 - Install Node.js 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: '20.9.0' 25 | 26 | - name: Step 3 - Build Frontend 27 | run: | 28 | cd web-app 29 | npm install 30 | NODE_OPTIONS="--max-old-space-size=1512" npm run build 31 | 32 | - name: Step 4 - Deploy to Dev Server 33 | uses: easingthemes/ssh-deploy@main 34 | env: 35 | SOURCE: "/" 36 | SSH_PRIVATE_KEY: ${{ secrets.AWS_DEV_EC2_SSH_KEY }} 37 | REMOTE_HOST: ${{ secrets.AWS_DEV_HOST_DNS }} 38 | REMOTE_USER: ${{ secrets.AWS_DEV_USERNAME }} 39 | TARGET: ${{ secrets.AWS_DEV_TARGET_DIR }} 40 | SCRIPT_AFTER: | 41 | set -x 42 | echo "Activate python env." 43 | cd /home/ubuntu/smartapi 44 | source .env/bin/activate 45 | echo "Installing backend requirements." 46 | pip install --upgrade pip 47 | pip install -Ur requirements.txt --no-cache-dir --ignore-installed --force-reinstall 48 | echo "Restarting smartapi backend services..." 49 | sudo systemctl restart smartapi@8000 50 | sleep 10 51 | sudo systemctl restart smartapi@8080 52 | echo "Smartapi backend services restarted!!!" 53 | set +x 54 | echo $RSYNC_STDOUT 55 | -------------------------------------------------------------------------------- /src/migrate.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from datetime import timezone 3 | 4 | from dateutil import parser 5 | from elasticsearch.client import Elasticsearch 6 | from elasticsearch.helpers import scan 7 | 8 | from controller.smartapi import SmartAPI 9 | from utils import decoder, indices 10 | 11 | ES_ORIGIN = "http://smart-api.info:9200" 12 | ES_DESTINATION = "http://localhost:9200" # CANNOT CHANGE THIS 13 | 14 | 15 | def migrate(): 16 | for doc in scan( 17 | Elasticsearch(ES_ORIGIN), 18 | query={"query": {"match_all": {}}}, 19 | index="smartapi_oas3", 20 | doc_type="api", 21 | ): 22 | print(doc["_id"]) 23 | if not doc["_source"]["_meta"].get("_archived"): 24 | url = doc["_source"]["_meta"]["url"] 25 | raw = decoder.decompress(base64.urlsafe_b64decode(doc["_source"]["~raw"])) 26 | 27 | smartapi = SmartAPI(url) 28 | smartapi.raw = raw 29 | smartapi.date_created = parser.parse(doc["_source"]["_meta"]["timestamp"]).replace(tzinfo=timezone.utc) 30 | smartapi.username = doc["_source"]["_meta"]["github_username"] 31 | smartapi.slug = doc["_source"]["_meta"].get("slug") 32 | smartapi.save() 33 | print() 34 | 35 | 36 | def update(): 37 | for doc in scan( 38 | Elasticsearch(ES_DESTINATION), 39 | query={"query": {"match_all": {}}}, 40 | index="smartapi_docs", 41 | scroll="60m", 42 | ): 43 | print(doc["_id"]) 44 | smartapi = SmartAPI.get(doc["_id"]) 45 | print(smartapi.check()) 46 | print(smartapi.refresh()) 47 | if smartapi.webdoc.status == 299: 48 | smartapi.webdoc._status = 200 # change status not reliable during migration 49 | smartapi.save() 50 | print() 51 | 52 | 53 | if __name__ == "__main__": 54 | input("Will reset smartapi_docs index. Ctrl-C to cancel.") 55 | indices.reset() 56 | migrate() 57 | indices.refresh() 58 | update() 59 | -------------------------------------------------------------------------------- /web-app/src/components/CopyButton.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 63 | 64 | 86 | -------------------------------------------------------------------------------- /web-app/public/img/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 23 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /web-app/src/assets/img/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 23 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /web-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "vite-app ready", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "test:unit": "vitest", 11 | "test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'", 12 | "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'", 13 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore", 14 | "format": "prettier --write src/" 15 | }, 16 | "dependencies": { 17 | "@cosmograph/cosmograph": "^1.3.1", 18 | "@meforma/vue-toaster": "^1.3.0", 19 | "axios": "^0.21.4", 20 | "chart.js": "^2.9.4", 21 | "clipboard": "^2.0.11", 22 | "core-js": "^3.33.0", 23 | "cytoscape": "^3.26.0", 24 | "cytoscape-popper": "^1.0.7", 25 | "graphology": "^0.25.4", 26 | "graphology-layout": "^0.6.1", 27 | "lodash": "^4.17.21", 28 | "mark.js": "^8.11.1", 29 | "marked": "^4.3.0", 30 | "material-design-icons": "^3.0.1", 31 | "materialize-css": "^1.0.0", 32 | "moment": "^2.29.4", 33 | "remarkable": "^2.0.1", 34 | "sigma": "^3.0.0-beta.6", 35 | "swagger-editor": "^4.13.1", 36 | "swagger-ui": "^5.10.3", 37 | "tabulator-tables": "^4.9.3", 38 | "tippy.js": "^6.3.7", 39 | "tsparticles-slim": "^2.12.0", 40 | "vivus": "^0.4.6", 41 | "vue": "^3.3.4", 42 | "vue-final-modal": "^1.8.8", 43 | "vue-gtag-next": "^1.14.0", 44 | "vue-meta": "^3.0.0-alpha.4", 45 | "vue-router": "^4.2.5", 46 | "vue-sweetalert2": "^4.3.1", 47 | "vue3-particles": "^2.12.0", 48 | "vuex": "^4.1.0", 49 | "zdog": "^1.1.3" 50 | }, 51 | "devDependencies": { 52 | "@rushstack/eslint-patch": "^1.5.1", 53 | "@vitejs/plugin-vue": "^4.4.0", 54 | "@vue/eslint-config-prettier": "^8.0.0", 55 | "@vue/test-utils": "^2.4.1", 56 | "cypress": "^13.3.1", 57 | "eslint": "^8.51.0", 58 | "eslint-plugin-cypress": "^2.15.1", 59 | "eslint-plugin-vue": "^9.17.0", 60 | "jsdom": "^22.1.0", 61 | "prettier": "^3.0.3", 62 | "start-server-and-test": "^2.0.1", 63 | "vite": "^4.4.11", 64 | "vite-plugin-sitemap": "^0.7.1", 65 | "vitest": "^0.34.6" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/utils/mapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "dynamic_templates": [ 3 | { 4 | "ignore_example_field": { 5 | "path_match": "*.example", 6 | "mapping": { 7 | "index": false, 8 | "type": "text" 9 | } 10 | } 11 | }, 12 | { 13 | "ignore_examples_field": { 14 | "match": "examples", 15 | "mapping": { 16 | "enabled": false 17 | } 18 | } 19 | }, 20 | { 21 | "ignore_ref_field": { 22 | "match": "$ref", 23 | "mapping": { 24 | "index": false 25 | } 26 | } 27 | }, 28 | { 29 | "ignore_schema_field": { 30 | "match": "schema", 31 | "mapping": { 32 | "enabled": false 33 | } 34 | } 35 | }, 36 | { 37 | "ignore_content_field": { 38 | "match": "content", 39 | "mapping": { 40 | "enabled": false 41 | } 42 | } 43 | }, 44 | { 45 | "ignore_default_field": { 46 | "match": "default", 47 | "mapping": { 48 | "type": "object", 49 | "enabled": false 50 | } 51 | } 52 | }, 53 | { 54 | "template_1": { 55 | "match": "*", 56 | "match_mapping_type": "string", 57 | "mapping": { 58 | "type": "text", 59 | "fields": { 60 | "raw": { 61 | "type": "keyword", 62 | "ignore_above": 512 63 | } 64 | }, 65 | "copy_to": "all" 66 | } 67 | } 68 | } 69 | ], 70 | "properties": { 71 | "components": { 72 | "enabled": false 73 | }, 74 | "definitions": { 75 | "enabled": false 76 | }, 77 | "_raw": { 78 | "type": "binary" 79 | }, 80 | "all": { 81 | "type": "text" 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /web-app/src/components/MetaHead.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 59 | -------------------------------------------------------------------------------- /src/handlers/oauth.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | from biothings.web.auth.oauth_mixins import GithubOAuth2Mixin 5 | from biothings.web.handlers import BaseAPIHandler 6 | from tornado.httputil import url_concat 7 | 8 | 9 | class GitHubLoginHandler(BaseAPIHandler, GithubOAuth2Mixin): 10 | """ "Handler for GitHub oauth login""" 11 | 12 | SCOPES = [] 13 | GITHUB_CALLBACK_PATH = "/oauth" 14 | 15 | async def get(self): 16 | CLIENT_ID = self.biothings.config.GITHUB_CLIENT_ID 17 | CLIENT_SECRET = self.biothings.config.GITHUB_CLIENT_SECRET 18 | code = self.get_argument("code", None) 19 | redirect_uri = url_concat( 20 | self.request.protocol + "://" + self.request.host + self.GITHUB_CALLBACK_PATH, 21 | {"next": self.get_argument("next", "/")}, 22 | ) 23 | if code is None: 24 | logging.info("Redirecting to login...") 25 | self.authorize_redirect( 26 | redirect_uri=redirect_uri, 27 | client_id=CLIENT_ID, 28 | scope=self.SCOPES, 29 | ) 30 | else: 31 | logging.info("got code, try to get token") 32 | token = await self.github_get_oauth2_token(client_id=CLIENT_ID, client_secret=CLIENT_SECRET, code=code) 33 | user = await self.github_get_authenticated_user(token["access_token"]) 34 | user = self._format_user_record(user) 35 | logging.info("Got user info: {}".format(user)) 36 | if user: 37 | logging.info("Setting user cookie.") 38 | self.set_secure_cookie("user", user) 39 | else: 40 | logging.info("Failed to get user info.") 41 | self.clear_cookie("user") 42 | self.redirect(self.get_argument("next", "/")) 43 | 44 | def _format_user_record(self, user): 45 | user_data = {} 46 | user_data["login"] = user.get("login") 47 | if not user_data["login"]: 48 | return 49 | if user.get("name"): 50 | user_data["name"] = user["name"] 51 | if user.get("email"): 52 | user_data["email"] = user["email"] 53 | if user.get("avatar_url"): 54 | user_data["avatar_url"] = user["avatar_url"] 55 | if user.get("company"): 56 | user_data["organization"] = user["company"] 57 | return json.dumps(user_data) 58 | -------------------------------------------------------------------------------- /src/utils/decoder.py: -------------------------------------------------------------------------------- 1 | """ Stream Decoder """ 2 | 3 | import gzip 4 | import json 5 | from hashlib import blake2b 6 | 7 | import yaml 8 | 9 | # ------------- 10 | # Conversion 11 | # ------------- 12 | 13 | TYPE_ERR = "Expect a serialization of a mapping type." 14 | 15 | 16 | def to_yaml(stream): 17 | try: 18 | data = yaml.load(stream, Loader=yaml.CSafeLoader) 19 | except (yaml.scanner.ScannerError, yaml.parser.ParserError) as err: 20 | raise ValueError(str(err)) from err 21 | if not isinstance(data, dict): 22 | raise TypeError(TYPE_ERR) 23 | return data 24 | 25 | 26 | def to_json(stream): 27 | try: 28 | data = json.loads(stream) 29 | except json.JSONDecodeError as err: 30 | raise ValueError(str(err)) from err 31 | if not isinstance(data, dict): 32 | raise TypeError(TYPE_ERR) 33 | return data 34 | 35 | 36 | def to_dict(stream, ext=None, ctype=None): 37 | """ 38 | Load a string or bytes to a dict. 39 | If extension or content-type is specified, 40 | only parse stream as the specified format. 41 | """ 42 | 43 | ext = ext.lower() if isinstance(ext, str) else "" 44 | ctype = ctype.lower() if isinstance(ctype, str) else "" 45 | 46 | # by extension 47 | if "json" in ext: 48 | return to_json(stream) 49 | if ext in ("yaml", "yml"): 50 | return to_yaml(stream) 51 | 52 | # by content-type 53 | if "json" in ctype: 54 | return to_json(stream) 55 | if "yaml" in ctype: 56 | return to_yaml(stream) 57 | 58 | # javascript files 59 | if isinstance(stream, bytes): 60 | try: 61 | stream = stream.decode() 62 | except UnicodeDecodeError: 63 | pass 64 | if isinstance(stream, str): 65 | if stream.startswith("export default "): 66 | stream = stream[len("export default "):] 67 | 68 | # brute force 69 | return to_yaml(stream) 70 | 71 | 72 | # ------------- 73 | # Compression 74 | # ------------- 75 | 76 | 77 | def compress(stream): 78 | return gzip.compress(stream) if stream else None 79 | 80 | 81 | def decompress(stream): 82 | return gzip.decompress(stream) if stream else None 83 | 84 | 85 | # ------------- 86 | # Hashed _id 87 | # ------------- 88 | def get_id(url): 89 | _bytes = str(url).encode("utf8") 90 | _hash = blake2b(_bytes, digest_size=16) 91 | return _hash.hexdigest() 92 | -------------------------------------------------------------------------------- /web-app/src/assets/img/clouds.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 14 | 18 | 22 | 26 | 27 | -------------------------------------------------------------------------------- /web-app/src/components/SimpleNetworkSigma.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 86 | -------------------------------------------------------------------------------- /web-app/src/assets/img/api-dryrun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/utils/notification.py: -------------------------------------------------------------------------------- 1 | """ 2 | API Registration Slack Notification Message 3 | https://api.slack.com/messaging/composing/layouts 4 | """ 5 | 6 | 7 | class SlackNewAPIMessage: 8 | def __init__(self, _id, name, description, username): 9 | self._id = _id 10 | self.name = name 11 | self.description = description 12 | self.username = username 13 | 14 | def get_notification(self): 15 | return f"A new API has been registered on Smart-API.info: {self.name}" 16 | 17 | def get_header(self): 18 | return { 19 | "type": "mrkdwn", 20 | "text": "A new API has been registered on Smart-API.info:", 21 | } 22 | 23 | def get_body(self): 24 | return { 25 | "type": "mrkdwn", 26 | "text": ( 27 | f"*Title*: {self.name}\n" 28 | f"*Description*: {self.description}\n" 29 | f"*Registered by*: " 30 | ), 31 | } 32 | 33 | def get_footer(self): 34 | return [ 35 | {"type": "mrkdwn", "text": f""}, 36 | {"type": "plain_text", "text": " | "}, 37 | {"type": "mrkdwn", "text": f""}, 38 | ] 39 | 40 | def compose(self): 41 | return { 42 | "text": self.get_notification(), 43 | "blocks": [ 44 | {"type": "section", "text": self.get_header()}, 45 | {"type": "section", "text": self.get_body()}, 46 | {"type": "context", "elements": self.get_footer()}, 47 | ], 48 | } 49 | 50 | 51 | class SlackNewTranslatorAPIMessage(SlackNewAPIMessage): 52 | def get_notification(self): 53 | return f"A new Translator API has been registered on Smart-API.info: {self.name}" 54 | 55 | def get_header(self): 56 | return { 57 | "type": "mrkdwn", 58 | "text": "A new Translator API has been registered on Smart-API.info:", 59 | } 60 | 61 | 62 | # ------------- 63 | # Tests 64 | # ------------- 65 | 66 | 67 | def test_slack(): 68 | """test slack notification on main channel""" 69 | import requests 70 | 71 | from config import SLACK_WEBHOOKS 72 | 73 | message = SlackNewAPIMessage("0xTEST", "MyAPI", "An API.", "tester") 74 | response = requests.post(SLACK_WEBHOOKS[0]["webhook"], json=message.compose()) 75 | print(response.status_code) 76 | print(response.text) 77 | 78 | 79 | if __name__ == "__main__": 80 | test_slack() 81 | -------------------------------------------------------------------------------- /web-app/src/assets/img/spec.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | -------------------------------------------------------------------------------- /src/ADMIN.md: -------------------------------------------------------------------------------- 1 | # Admin.py Usage Guide 2 | 3 | The `admin.py` file is a data administration module. Here the indices are built, populated, restored and updated. 4 | 5 | ## Restore Smart API 6 | 7 | #### `restore_from_file(filename=file)` 8 | - **Description:** Used to restore Smart APIs from a JSON file. It reads the data from the specified JSON file and restores the Smart APIs accordingly. 9 | - **Parameters:** 10 | - `filename` (str): The path to the JSON file containing Smart API data. 11 | 12 | ```python 13 | from admin import restore_from_file 14 | 15 | file="/path/to/file.json" 16 | restore_from_file(filename=file) 17 | ``` 18 | 19 | #### `restore_from_s3(filename=None, bucket="smartapi")` 20 | 21 | - **Description:** Used to restore Smart APIs from an AWS S3 bucket. It retrieves the latest backup file from the specified bucket and restores the Smart APIs. 22 | - **Parameters:** 23 | - `filename` (str, optional): The name of the backup file in the S3 bucket. If not provided, it retrieves the latest backup file. 24 | - `bucket` (str, optional): The name of the S3 bucket where backup files are stored. Default is "smartapi". 25 | 26 | 27 | ## Building the MetaKG Index 28 | 29 | #### `refresh_metakg(reset=True, include_trapi=True)` 30 | 31 | - **Description:** Refreshes the MetaKG index. 32 | - **Parameters:** 33 | - `reset` (optional, default=True): If True, resets the MetaKG index. 34 | - `include_trapi` (optional, default=True): If True, includes TRAPI (Translator Reasoning API) data during the refresh. 35 | 36 | ```python 37 | from admin import refresh_metakg 38 | refresh_metakg() 39 | ``` 40 | 41 | ## Building the ConsolidatedMetaKG Index 42 | 43 | #### `consolidate_metakg(reset=True)` 44 | 45 | - **Description:** Consolidates MetaKG edge data into documents based on a subject-predicate-object key. It creates an index with the groups, facilitating efficient access and management of MetaKG-related information. 46 | - **Parameters:** 47 | - `reset` (optional, default=True): If True, resets the ConsolidatedMetaKG index. 48 | 49 | Note: the index, `smartapi_docs_metakg`, **must be available** --build with `refresh_metakg()`. 50 | 51 | ```python 52 | # if not run already - 53 | from admin import refresh_metakg 54 | refresh_metakg() 55 | # then run to build consolidated index 56 | from admin import consolidate_metakg 57 | consolidate_metakg() 58 | ``` 59 | 60 | #### `refresh_has_metakg()` 61 | 62 | - **Description:** Updates the '`has_metakg`' attribute for SmartAPI objects. It iterates through all SmartAPI objects, verifying their existence in the Meta-Knowledge Graph and updating the '`has_metakg`' attribute accordingly. 63 | 64 | ```python 65 | from admin import refresh_has_metakg 66 | refresh_has_metakg 67 | ``` -------------------------------------------------------------------------------- /web-app/src/assets/img/api-fail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 47 | 48 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/tests/validate/test_validate.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from controller.base import openapis, swaggers, validate 5 | 6 | dirname = os.path.dirname(__file__) 7 | 8 | with open(os.path.join(dirname, "openapi-pass.json"), "rb") as file: 9 | PASS_OPENAPI = file.read() 10 | 11 | with open(os.path.join(dirname, "openapi3.1-pass.json"), "rb") as file: 12 | PASS_OPENAPI31 = file.read() 13 | 14 | with open(os.path.join(dirname, "openapi3.1-fail.json"), "rb") as file: 15 | FAIL_OPENAPI31 = file.read() 16 | 17 | with open(os.path.join(dirname, "swagger-pass.json"), "rb") as file: 18 | PASS_SWAGGER = file.read() 19 | 20 | with open(os.path.join(dirname, "x-translator-pass.json"), "rb") as file: 21 | PASS_TRANSLATOR = file.read() 22 | 23 | with open(os.path.join(dirname, "x-translator-fail-1.yml"), "rb") as file: 24 | FAIL_TRANSLATOR_1 = file.read() 25 | 26 | with open(os.path.join(dirname, "x-translator-fail-2.yml"), "rb") as file: 27 | FAIL_TRANSLATOR_2 = file.read() 28 | 29 | 30 | def test_01(): 31 | validate(PASS_OPENAPI, {"openapi_v3.0": openapis["openapi_v3.0"]}) 32 | validate(PASS_OPENAPI, openapis) 33 | 34 | 35 | def test_02(): 36 | validate(PASS_TRANSLATOR, {"openapi_v3.0": openapis["openapi_v3.0"]}) 37 | validate( 38 | PASS_TRANSLATOR, 39 | { 40 | "openapi_v3.0": openapis["openapi_v3.0"], 41 | "x-translator": openapis["x-translator"], 42 | }, 43 | ) 44 | validate(PASS_TRANSLATOR, openapis) 45 | 46 | 47 | def test_03(): 48 | validate(FAIL_TRANSLATOR_1, {"openapi_v3.0": openapis["openapi_v3.0"]}) 49 | with pytest.raises(ValueError, match="Failed x-translator validation"): 50 | validate(FAIL_TRANSLATOR_1, openapis) 51 | 52 | 53 | def test_04(): 54 | validate(FAIL_TRANSLATOR_2, {"openapi_v3.0": openapis["openapi_v3.0"]}) 55 | with pytest.raises(ValueError, match="Failed x-translator validation"): 56 | validate(FAIL_TRANSLATOR_2, openapis) 57 | 58 | 59 | def test_05(): 60 | validate(PASS_SWAGGER, swaggers) 61 | with pytest.raises(ValueError): 62 | validate(PASS_SWAGGER, openapis) 63 | with pytest.raises(ValueError): 64 | validate(PASS_OPENAPI, swaggers) 65 | 66 | 67 | def test_06(): 68 | validate(PASS_OPENAPI31, {"openapi_v3.1": openapis["openapi_v3.1"]}) 69 | validate(PASS_OPENAPI31, openapis) 70 | 71 | 72 | def test_07(): 73 | with pytest.raises(ValueError): 74 | validate(FAIL_OPENAPI31, {"openapi_v3.1": openapis["openapi_v3.1"]}) 75 | with pytest.raises(ValueError): 76 | validate(FAIL_OPENAPI31, openapis) 77 | 78 | 79 | def test_08(): 80 | # should fail when no version-matching openapi schema to validate 81 | with pytest.raises(ValueError, match="Unknown OpenAPI version"): 82 | validate(PASS_OPENAPI31, {"openapi_v3.0": openapis["openapi_v3.0"]}) 83 | -------------------------------------------------------------------------------- /src/tests/format/test_format.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import pytest 5 | 6 | from controller.base import OpenAPI, Swagger 7 | 8 | dirname = os.path.dirname(__file__) 9 | 10 | with open(os.path.join(dirname, "swagger.json"), "r") as file: 11 | SWAGGER = json.load(file) 12 | 13 | with open(os.path.join(dirname, "openapi.json"), "r") as file: 14 | OPENAPI = json.load(file) 15 | 16 | 17 | def aligns(keys, reference): 18 | common = set(keys) & set(reference) 19 | keys = tuple(key for key in keys if key in common) 20 | refs = tuple(key for key in reference if key in common) 21 | return keys == refs 22 | 23 | 24 | def test_swagger(): 25 | swagger = Swagger(SWAGGER) 26 | assert tuple(swagger.keys())[: len(Swagger.KEYS)] == Swagger.KEYS 27 | swagger.validate() 28 | swagger["_metadata"] = {} 29 | swagger.data.move_to_end("_metadata", False) # to front 30 | assert tuple(swagger.keys())[: len(Swagger.KEYS)] != Swagger.KEYS 31 | assert tuple(swagger.keys())[0] == "_metadata" 32 | swagger.order() 33 | assert tuple(swagger.keys())[: len(Swagger.KEYS)] == Swagger.KEYS 34 | with pytest.raises(ValueError): 35 | swagger.validate() # _metadata is an invalid key 36 | assert tuple(swagger.keys()) != Swagger.KEYS 37 | swagger.clean() 38 | assert tuple(swagger.keys()) == Swagger.KEYS 39 | with pytest.raises(ValueError): 40 | # "paths" key is removed for ES indexing 41 | swagger.validate() 42 | 43 | 44 | def test_openapi(): 45 | openapi = OpenAPI(OPENAPI) 46 | assert aligns(openapi.keys(), OpenAPI.KEYS) 47 | openapi["_metadata"] = {} 48 | openapi.data.move_to_end("_metadata", False) # to front 49 | assert tuple(openapi.keys())[0] == "_metadata" 50 | assert aligns(openapi.keys(), OpenAPI.KEYS) 51 | openapi.data.move_to_end("openapi") 52 | assert not aligns(openapi.keys(), OpenAPI.KEYS) 53 | openapi.order() 54 | assert aligns(openapi.keys(), OpenAPI.KEYS) 55 | with pytest.raises(ValueError): 56 | openapi.validate() # _metadata is an invalid key 57 | del openapi["_metadata"] 58 | openapi.validate() 59 | assert tuple(openapi.keys()) != OpenAPI.KEYS 60 | assert isinstance(openapi["paths"], dict) 61 | assert "/gene" in openapi["paths"] 62 | assert "/query" in openapi["paths"] 63 | assert "/metadata" in openapi["paths"] 64 | openapi.transform() 65 | openapi.clean() 66 | assert aligns(openapi.keys(), OpenAPI.KEYS) 67 | assert not set(openapi.keys()) - set(OpenAPI.KEYS) 68 | assert isinstance(openapi["paths"], list) 69 | assert openapi["paths"][0]["path"] == "/gene" 70 | assert openapi["paths"][2]["path"] == "/metadata" 71 | assert openapi["paths"][4]["path"] == "/query" 72 | with pytest.raises(ValueError): 73 | # "paths" is transformed for ES indexing 74 | openapi.validate() 75 | -------------------------------------------------------------------------------- /web-app/src/views/FAQ.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 80 | 81 | 90 | -------------------------------------------------------------------------------- /src/tests/test_model.py: -------------------------------------------------------------------------------- 1 | """ 2 | SmartAPI Database Persistence Model Tests 3 | """ 4 | import json 5 | import os 6 | 7 | import pytest 8 | from model import SmartAPIDoc 9 | from utils import decoder 10 | from utils.indices import delete, refresh, reset 11 | 12 | dirname = os.path.dirname(__file__) 13 | 14 | # prepare data to be saved in tests 15 | with open(os.path.join(dirname, "mygene.es.json"), "r") as file: 16 | MYGENE_ES = json.load(file) 17 | MYGENE_ID = MYGENE_ES.pop("_id") 18 | 19 | with open(os.path.join(dirname, "mygene.es.json"), "rb") as file: 20 | MYGENE_RAW = file.read() 21 | 22 | ES_INDEX_NAME = "smartapi_docs_test" 23 | 24 | 25 | @pytest.fixture(autouse=True, scope="module") 26 | def setup_fixture(): 27 | reset(SmartAPIDoc, index=ES_INDEX_NAME) 28 | mygene = SmartAPIDoc(meta={"id": "doc1"}, **MYGENE_ES) 29 | mygene._raw = decoder.compress(MYGENE_RAW) 30 | mygene.save(index=ES_INDEX_NAME) 31 | refresh(index=ES_INDEX_NAME) 32 | 33 | 34 | def test_exists(): 35 | # info.title : "MyDisease.info API" 36 | assert not SmartAPIDoc.exists("doc0", index=ES_INDEX_NAME) 37 | assert SmartAPIDoc.exists("doc1", index=ES_INDEX_NAME) 38 | assert SmartAPIDoc.exists("3.0.0", "openapi", index=ES_INDEX_NAME) 39 | assert SmartAPIDoc.exists("mygene", "_meta.slug", index=ES_INDEX_NAME) 40 | assert not SmartAPIDoc.exists("mygene", "info.title", index=ES_INDEX_NAME) 41 | assert SmartAPIDoc.exists("mygene.info", "info.title", index=ES_INDEX_NAME) 42 | assert SmartAPIDoc.exists("mygene.info api", "info.title", index=ES_INDEX_NAME) 43 | assert SmartAPIDoc.exists("api", "info.title", index=ES_INDEX_NAME) 44 | assert not SmartAPIDoc.exists("mygene", "info.title.raw", index=ES_INDEX_NAME) 45 | assert not SmartAPIDoc.exists("mygene.info", "info.title.raw", index=ES_INDEX_NAME) 46 | assert not SmartAPIDoc.exists("mygene.info api", "info.title.raw", index=ES_INDEX_NAME) 47 | assert not SmartAPIDoc.exists("api", "info.title.raw", index=ES_INDEX_NAME) 48 | assert SmartAPIDoc.exists("MyGene.info API", "info.title.raw", index=ES_INDEX_NAME) 49 | assert SmartAPIDoc.exists("mygene.info", "info.description", index=ES_INDEX_NAME) 50 | 51 | 52 | def test_aggregation(): 53 | assert "Chunlei Wu" in SmartAPIDoc.aggregate("info.contact.name", index=ES_INDEX_NAME) 54 | assert "gene" in SmartAPIDoc.aggregate(index=ES_INDEX_NAME) 55 | assert "annotation" in SmartAPIDoc.aggregate(index=ES_INDEX_NAME) 56 | assert "query" in SmartAPIDoc.aggregate(index=ES_INDEX_NAME) 57 | assert "query" in SmartAPIDoc.aggregate("tags.name.raw", index=ES_INDEX_NAME) 58 | assert "query" in SmartAPIDoc.aggregate("tags.name", index=ES_INDEX_NAME) 59 | assert "tester" in SmartAPIDoc.aggregate("_meta.username", index=ES_INDEX_NAME) 60 | assert "mygene" in SmartAPIDoc.aggregate("_meta.slug", index=ES_INDEX_NAME) 61 | 62 | 63 | def teardown_module(): 64 | delete(index=ES_INDEX_NAME) 65 | -------------------------------------------------------------------------------- /src/tests/decoder/doc_mydisease.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "etag": "659023576fb3c74c5a6eb4ec3987fba0d4164f060ab33c5040c0386ac18544b2", 4 | "username": "cyrus0824", 5 | "slug": "mydisease", 6 | "timestamp": "2019-10-22T04:26:58.530282", 7 | "url": "https://raw.githubusercontent.com/biothings/mydisease.info/master/mydisease/swagger/mydisease.yml" 8 | }, 9 | "openapi": "3.0.0", 10 | "info": { 11 | "version": "1.0", 12 | "title": "MyDisease.info API", 13 | "description": "Documentation of the MyDisease.info disease query web services. Learn more about [MyDisease.info](http://MyDisease.info/)", 14 | "termsOfService": "http://MyDisease.info/terms", 15 | "contact": { 16 | "name": "Chunlei Wu", 17 | "x-role": "responsible developer", 18 | "email": "help@biothings.io", 19 | "x-id": "https://github.com/newgene" 20 | } 21 | }, 22 | "servers": [ 23 | { 24 | "url": "http://MyDisease.info/v1", 25 | "description": "Production server" 26 | } 27 | ], 28 | "tags": [ 29 | { 30 | "name": "disease" 31 | }, 32 | { 33 | "name": "query" 34 | }, 35 | { 36 | "name": "metadata" 37 | } 38 | ], 39 | "paths": { 40 | "/metadata/fields": { 41 | "get": { 42 | "tags": [ 43 | "metadata" 44 | ], 45 | "summary": "Get metadata about the data fields available from a MyDisease.info disease object", 46 | "parameters": [ 47 | { 48 | "name": "search", 49 | "$ref": "#/components/parameters/search" 50 | } 51 | ], 52 | "responses": { 53 | "200": { 54 | "description": "MyDisease.info metadata fields object" 55 | } 56 | } 57 | } 58 | } 59 | }, 60 | "components": { 61 | "parameters": { 62 | "search": { 63 | "name": "search", 64 | "in": "query", 65 | "description": "Pass a search term to filter the available fields. Type: string. Default: None.", 66 | "schema": { 67 | "type": "string" 68 | } 69 | } 70 | }, 71 | "schemas": { 72 | "string_or_array": { 73 | "oneOf": [ 74 | { 75 | "items": { 76 | "type": "string" 77 | }, 78 | "type": "array" 79 | }, 80 | { 81 | "type": "string" 82 | } 83 | ] 84 | } 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /docs/CREATE_API.md: -------------------------------------------------------------------------------- 1 | # General API implementation best practices 2 | Regardless the type of APIs you are implementing, we recommend to follow these API best-practices. 3 | 4 | 5 | ### 1. CORS support 6 |      An API endpoint should support [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) with unrestricted hostnames, so that users can make cross-origin API requests directly from their web application. 7 | 8 | ### 2. HTTPS support 9 |      An API endpoint should support HTPPS protocol, ideally both HTTP and HTTPS, so that users can make encrypted API request if needed. 10 | 11 | ### 3. HTTP compression support 12 |      An API endpoint should support gzip HTTP compression protocol to reduce the data transfer size. 13 | 14 | ### 4. HTTP caching support 15 |      An API endpoint support HTTP caching headers with both “Cache-Control” and “etag” headers (max-age can be adjustable, e.g. set to 7 days). 16 | 17 | ### 5. Versioning 18 |       To maintain the backcompatability, versioning your API is preferred. One way is to include the version number in the URLs: 19 | - Include the version number (as "v1", "v2", "v3", and so on) to the endpoint URLs (e.g. http://myvariant.info/v1/variant endpoint) 20 | - Increase version number when breaking changes are introduced to the API 21 | 22 | ### 6. Support batch queries 23 |      Batch queries are efficient for a large list of inputs. A typical implementation is to use GET for single query, and POST for the corresponding batch query: 24 | - GET: perform a single entity-retrieval or a single query 25 | - POST: perform a batch of entity-retrieval or queries 26 | 27 | ### 7. Support JSON response format 28 |      Typically, an API can return response in JSON format by default. If multiple return formats are available, one can consider to use "content-type" header or explicit query parameter: 29 | - `Content-Type` header 30 | 31 | When ```Content-Type: application/json``` header is present in the request, the response will be returned as JSON. 32 | 33 | - Query parameter: 34 | 35 | For example, when `format=json` query parameter is passed, the response will be returned as JSON. 36 | 37 | 38 | ### 8. Support paging or streaming of the large response 39 | 40 | * If a large response is expected from an API endpoint, the paging or streaming support is preferred. For example, this is a typical paging implementation with both `size` and `from` query parameters: 41 | 42 | * size 43 | * The maximum number of matching object hits to return 44 | * Optional, default is 10 45 | 46 | * from 47 | * The number of matching hits to skip 48 | * Optional, default is 0 49 | 50 | See this live example from [MyGene.info](https://mygene.info) API: 51 | 52 | https://mygene.info/v3/query?q=cdk2&size=50&from=20 53 | 54 | -------------------------------------------------------------------------------- /web-app/src/components/SimpleNetworkCosmo.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 91 | 92 | 99 | -------------------------------------------------------------------------------- /web-app/src/assets/img/api-welcome.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 70 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /web-app/src/assets/img/uptodate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 21 | 27 | 30 | 32 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /web-app/src/views/Editor.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 74 | 75 | 108 | -------------------------------------------------------------------------------- /web-app/src/components/Navigation.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 67 | 68 | 111 | -------------------------------------------------------------------------------- /src/tests/_utils/metakg/integration/parser/index_test.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import unittest 4 | 5 | from utils.metakg.api import API 6 | 7 | 8 | class TestAPIParser(unittest.TestCase): 9 | def setUp(self): 10 | mygene_file_path = os.path.abspath( 11 | os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, "data", "mygene.json") 12 | ) 13 | with open(mygene_file_path, encoding="utf-8") as f: 14 | mygene_doc = json.load(f) 15 | mygene = API(mygene_doc) 16 | self.metadata = mygene.metadata 17 | 18 | def test_parse_API_name(self): 19 | self.assertEqual(self.metadata["title"], "MyGene.info API") 20 | 21 | def test_parse_API_tags(self): 22 | self.assertIn("biothings", self.metadata["tags"]) 23 | 24 | def test_parse_component(self): 25 | self.assertIsNotNone(self.metadata["components"]) 26 | 27 | def test_fetch_meta_data(self): 28 | self.assertEqual(self.metadata["title"], "MyGene.info API") 29 | self.assertIn("biothings", self.metadata["tags"]) 30 | self.assertEqual(self.metadata["url"], "https://mygene.info/v3") 31 | self.assertIsNotNone(self.metadata["components"]) 32 | 33 | def test_fetch_all_operations(self): 34 | ops = self.metadata["operations"] 35 | self.assertEqual(ops[0]["association"]["api_name"], "MyGene.info API") 36 | 37 | 38 | class TestAPIParserWhichIsAlreadyDereferenced(unittest.TestCase): 39 | def setUp(self): 40 | opentarget_file_path = os.path.abspath( 41 | os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, "data", "opentarget.json") 42 | ) 43 | with open(opentarget_file_path, encoding="utf-8") as f: 44 | smartapi_spec = json.load(f) 45 | opentarget = API(smartapi_spec) 46 | self.metadata = opentarget.metadata 47 | 48 | def test_parse_component(self): 49 | self.assertEqual(self.metadata["components"], None) 50 | 51 | def test_fetch_meta_data(self): 52 | self.assertEqual(self.metadata["title"], "OpenTarget API") 53 | self.assertIn("translator", self.metadata["tags"]) 54 | self.assertEqual(self.metadata["url"], "https://platform-api.opentargets.io/v3") 55 | self.assertEqual(self.metadata["components"], None) 56 | 57 | def test_fetch_all_operations(self): 58 | ops = self.metadata["operations"] 59 | self.assertEqual(ops[0]["association"].get("api_name"), "OpenTarget API") 60 | self.assertEqual(ops[0]["association"].get("predicate"), "biolink:related_to") 61 | self.assertEqual(ops[0]["association"].get("input_id"), "biolink:ENSEMBL") 62 | self.assertEqual(ops[0]["query_operation"]["path"], "/platform/public/evidence/filter") 63 | 64 | 65 | class TestAPIParserUsingSpecsWithParameters(unittest.TestCase): 66 | def test_path_params(self): 67 | litvar_file_path = os.path.abspath( 68 | os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, "data", "litvar.json") 69 | ) 70 | with open(litvar_file_path, encoding="utf-8") as f: 71 | smartapi_spec = json.load(f) 72 | litvar = API(smartapi_spec) 73 | path_params = litvar.metadata["operations"][0]["query_operation"]["path_params"] 74 | self.assertEqual(path_params, ["variantid"]) 75 | -------------------------------------------------------------------------------- /src/tests/decoder/test_decoder.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from utils import decoder 6 | 7 | dirname = os.path.dirname(__file__) 8 | 9 | with open(os.path.join(dirname, "doc_mydisease.yaml"), "rb") as file: 10 | YAML = file.read() 11 | 12 | with open(os.path.join(dirname, "doc_mydisease.json"), "rb") as file: 13 | JSON = file.read() 14 | 15 | with open(os.path.join(dirname, "doc_javascript.ts"), "rb") as file: 16 | JSTS = file.read() 17 | 18 | with open(os.path.join(dirname, "doc_swagger2.js"), "rb") as file: 19 | SWAGGER = file.read() 20 | 21 | 22 | def _ok(doc): 23 | assert "openapi" in doc 24 | assert "info" in doc 25 | assert "paths" in doc 26 | 27 | 28 | def test_yaml_good(): 29 | _ok(decoder.to_yaml(YAML)) 30 | # practically uncommon 31 | assert decoder.to_yaml(b"{}") == {} 32 | # but JSON is a subset of YAML 33 | _ok(decoder.to_yaml(JSON)) 34 | 35 | 36 | def test_yaml_bad(): 37 | with pytest.raises(TypeError): # NOTE TypeError 38 | decoder.to_yaml(b"") 39 | with pytest.raises(TypeError): 40 | decoder.to_yaml(b"/\\") 41 | with pytest.raises(ValueError): 42 | decoder.to_yaml(JSTS) 43 | 44 | 45 | def test_json_good(): 46 | _ok(decoder.to_json(JSON)) 47 | assert decoder.to_json(b"{}") == {} 48 | 49 | 50 | def test_json_bad(): 51 | with pytest.raises(TypeError): 52 | decoder.to_json(b"[]") 53 | with pytest.raises(ValueError): # NOTE ValueError 54 | decoder.to_json(b"") 55 | with pytest.raises(ValueError): 56 | decoder.to_json(YAML) 57 | with pytest.raises(ValueError): 58 | decoder.to_json(JSTS) 59 | 60 | 61 | def test_auto_good(): 62 | _ok(decoder.to_dict(YAML)) 63 | _ok(decoder.to_dict(JSON)) 64 | _ok(decoder.to_dict(JSTS)) # NOTE Javascript Only Supported Here 65 | 66 | _ok(decoder.to_dict(JSON, "json")) 67 | _ok(decoder.to_dict(JSON, ctype="application/json")) 68 | _ok(decoder.to_dict(JSON, ctype="application/JSON")) 69 | _ok(decoder.to_dict(JSON, "json", "application/json")) 70 | _ok(decoder.to_dict(JSON, ctype="application/json; charset=utf-8")) 71 | 72 | _ok(decoder.to_dict(YAML, "yml")) 73 | _ok(decoder.to_dict(YAML, "yaml")) 74 | _ok(decoder.to_dict(YAML, ctype="application/yaml")) 75 | _ok(decoder.to_dict(YAML, ctype="application/YAML")) 76 | _ok(decoder.to_dict(YAML, "yaml", "application/yaml")) 77 | _ok(decoder.to_dict(YAML, ctype="application/yaml; charset=utf-8")) 78 | 79 | 80 | def test_auto_bad(): 81 | with pytest.raises(TypeError): # NOTE TypeError 82 | decoder.to_dict(b"") 83 | with pytest.raises(TypeError): 84 | decoder.to_dict(b"[]") 85 | with pytest.raises(TypeError): 86 | decoder.to_dict(b"/\\") 87 | with pytest.raises(ValueError): 88 | decoder.to_dict(YAML, "json") 89 | with pytest.raises(ValueError): 90 | decoder.to_dict(YAML, ctype="application/json") 91 | 92 | 93 | def test_javascript(): 94 | # intentionally test this because 95 | # we rely on it in the application 96 | doc = decoder.to_dict(SWAGGER) 97 | assert doc["id"] == "http://swagger.io/v2/schema.json#" 98 | assert doc["type"] == "object" 99 | assert "$schema" in doc 100 | assert "required" in doc 101 | -------------------------------------------------------------------------------- /src/tests/_utils/metakg/data/litvar.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "LitVar API", 6 | "description": "LitVar allows the search and retrieval of variant relevant information from the biomedical literature and shows key biological relations between a variant and its close related entities (e.g. genes, diseases, and drugs). The LitVar results are automatically extracted (with regular updates) from over 27 million PubMed articles as well as applicable full-text articles in PubMed Central.", 7 | "termsOfService": "https://www.ncbi.nlm.nih.gov/home/about/policies/", 8 | "contact": { 9 | "name": "Zhiyong Lu", 10 | "email": "luzh@ncbi.nlm.nih.gov" 11 | } 12 | }, 13 | "servers": [ 14 | { 15 | "url": "https://www.ncbi.nlm.nih.gov/research/bionlp/litvar/api/v1", 16 | "description": "Production server" 17 | } 18 | ], 19 | "tags": [ 20 | { 21 | "name": "variant" 22 | }, 23 | { 24 | "name": "translator" 25 | } 26 | ], 27 | "paths": { 28 | "/entity/litvar/{variantid}": { 29 | "get": { 30 | "summary": "Retrieve PMIDs of publications mentioning submitted variants", 31 | "parameters": [ 32 | { 33 | "name": "variantid", 34 | "in": "path", 35 | "example": "rs121913527", 36 | "description": "rsid", 37 | "required": true, 38 | "schema": { 39 | "type": "string" 40 | } 41 | } 42 | ], 43 | "responses": { 44 | "200": { 45 | "description": "publications mentioned the submitted variant" 46 | } 47 | }, 48 | "x-bte-kgs-operations": [ 49 | { 50 | "$ref": "#/components/x-bte-kgs-operations/variant_located_in_gene" 51 | } 52 | ] 53 | } 54 | } 55 | }, 56 | "components": { 57 | "x-bte-kgs-operations": { 58 | "variant_located_in_gene": [ 59 | { 60 | "inputs": [ 61 | { 62 | "id": "DBSNP", 63 | "semantic": "SequenceVariant" 64 | } 65 | ], 66 | "outputs": [ 67 | { 68 | "id": "SYMBOL", 69 | "semantic": "Gene" 70 | } 71 | ], 72 | "predicate": "located_in", 73 | "source": "dbsnp", 74 | "parameters": { 75 | "variantid": "{inputs[0]}%23%23" 76 | }, 77 | "supportBatch": false, 78 | "response_mapping": { 79 | "$ref": "#/components/x-bte-response-mapping/variant_located_in_gene" 80 | } 81 | } 82 | ] 83 | }, 84 | "x-bte-response-mapping": { 85 | "variant_located_in_gene": { 86 | "SYMBOL": "gene.name" 87 | } 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /src/tests/test_query.py: -------------------------------------------------------------------------------- 1 | """ 2 | Biothings ESQueryHandler Type Tester 3 | """ 4 | import pytest 5 | from biothings.tests.web import BiothingsWebAppTest 6 | from controller import SmartAPI 7 | from utils.indices import delete, refresh, reset 8 | 9 | ES_INDEX_NAME = "smartapi_docs_test" 10 | 11 | MYGENE_URL = ( 12 | "https://raw.githubusercontent.com/NCATS-Tangerine/translator-api-registry/master/mygene.info/openapi_minimum.yml" 13 | ) 14 | MYCHEM_URL = ( 15 | "https://raw.githubusercontent.com/NCATS-Tangerine/translator-api-registry/master/mychem.info/openapi_full.yml" 16 | ) 17 | 18 | MYGENE_ID = "67932b75e2c51d1e1da2bf8263e59f0a" 19 | MYCHEM_ID = "8f08d1446e0bb9c2b323713ce83e2bd3" 20 | 21 | 22 | @pytest.fixture(scope="module", autouse=True) 23 | def setup(): 24 | """ 25 | setup state called once for the class 26 | """ 27 | SmartAPI.INDEX = ES_INDEX_NAME 28 | reset(SmartAPI.MODEL_CLASS, index=ES_INDEX_NAME) 29 | 30 | mygene = SmartAPI(MYGENE_URL) 31 | mygene.raw = b"{\"test\": null}" 32 | mygene.username = "tester" 33 | mygene.refresh() 34 | mygene.check() 35 | mygene.save() 36 | 37 | mychem = SmartAPI(MYCHEM_URL) 38 | mychem.raw = b"{\"test\": null}" 39 | mychem.username = "tester" 40 | mychem.refresh() 41 | mychem.check() 42 | mychem.save() 43 | 44 | refresh(index=ES_INDEX_NAME) 45 | 46 | 47 | # @pytest.mark.skip("All tests failed by 404 response") 48 | class SmartAPIQueryTest(BiothingsWebAppTest): 49 | prefix = "" 50 | 51 | def query(self, method="GET", endpoint="/api/query", **kwargs): 52 | return super().query(method=method, endpoint=endpoint, **kwargs) 53 | 54 | def test_match_all(self): 55 | res = self.query() 56 | assert res["total"] == 2 57 | res = self.query(q="__all__") 58 | assert res["total"] == 2 59 | 60 | def test_query_string(self): 61 | res = self.query(q="mygene") 62 | assert res["total"] == 1 63 | res = self.query(q="mychem") 64 | assert res["total"] == 1 65 | 66 | # since we have a predefined scoring method, 67 | # the order of this query is well defined 68 | res = self.query(q="query gene", meta=1, fields="_meta") 69 | assert res["hits"][0]["_id"] == MYGENE_ID 70 | assert res["hits"][1]["_id"] == MYCHEM_ID 71 | 72 | res = self.query(q="tags.name:translator") 73 | assert res["total"] == 2 74 | res = self.query(q="tags.name:chemical") 75 | assert res["total"] == 1 76 | res = self.query(q="tags.name:gene") 77 | assert res["total"] == 1 78 | 79 | def test_query_filters(self): 80 | res = self.query(authors='"Chunlei Wu"') 81 | assert res["total"] == 2 82 | res = self.query(authors="otheruser", hits=False) 83 | assert res["total"] == 0 84 | res = self.query(tags="translator") 85 | assert res["total"] == 2 86 | res = self.query(tags="chemical") 87 | assert res["total"] == 1 88 | res = self.query(tags="gene") 89 | assert res["total"] == 1 90 | res = self.query(authors='"Chunlei Wu"', tags="translator") 91 | assert res["total"] == 2 92 | res = self.query(authors='"Chunlei Wu"', tags="gene,chemical") 93 | assert res["total"] == 2 94 | res = self.query(authors="otheruser", tags="gene,chemical", hits=False) 95 | assert res["total"] == 0 96 | 97 | 98 | def teardown_module(): 99 | delete(SmartAPI.MODEL_CLASS, index=SmartAPI.INDEX) 100 | SmartAPI.INDEX = None 101 | -------------------------------------------------------------------------------- /web-app/src/assets/img/faq.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 36 | 37 | 38 | 39 | 41 | 42 | 43 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /web-app/src/assets/img/api-sucess.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | 46 | 47 | 48 | 51 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SmartAPI 2 | Intelligent APIs for a more connected web. 3 | 4 | A BD2K/Network-of-BioThings project. 5 | 6 | SmartAPI allows API publishers to annotate their services and input/output parameters in a structured and identifiable manner, based on a standard JSON-LD format for biomedical APIs and services. By indexing and visualizing these descriptions as Linked Data in a Elasticsearch back-end, researchers can seamlessly identify the services that consume or return desired parameters, and automatically compose services in workflows that yield new insights. 7 | 8 | Presentation: http://bit.ly/smartAPIslides 9 | Contact: api-interoperability@googlegroups.com 10 | 11 | 12 | # How to run a dev API server locally 13 | 1. Install Elasticsearch (version 7.x) at localhost:9200 (follow [this instruction](https://www.elastic.co/downloads/elasticsearch)) or install with docker (follow [this instruction](https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html)) 14 | 2. Clone this repo 15 | ``` 16 | git clone https://github.com/SmartAPI/smartAPI.git 17 | ```` 18 | 3. Install system packages (on Ubuntu, for example) 19 | ``` 20 | sudo apt install libcurl4-openssl-dev libssl-dev aws-cli 21 | ``` 22 | 4. Install python dependencies after navigating to root smartAPI directory 23 | ``` 24 | cd smartAPI 25 | pip install -r requirements.txt 26 | ``` 27 | 5. Navigate to SmartAPI source files and create a *config_key.py* under *src* 28 | ``` 29 | cd src 30 | touch config_key.py 31 | ``` 32 | 6. Update *config_key.py* with 33 | ``` 34 | COOKIE_SECRET = '' 35 | GITHUB_CLIENT_ID = '' 36 | GITHUB_CLIENT_SECRET = '' 37 | SLACK_WEBHOOKS = [ 38 | { 39 | "tag": '', # (optional) 40 | "webhook": '' 41 | } 42 | ] # (optional) 43 | ``` 44 | For Github incorporation, follow [this instruction](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) to create your Github Client ID and Secret. 45 | Enter any _Application name_, `http://localhost:8000/` for _Homepage 46 | URL_ and `http://localhost:8000/oauth` for _Authorization callback URL_. 47 | 48 | For SLACK_WEBHOOKS (optional), the list may not be included if one does not want Slack notifications pushed every time a new API is added to the smartAPI registry. 49 | 50 | Alternatively, if one wants slack notifications sent to more than one channel, one may list more than one dict in the ```SLACK_WEBHOOKS``` list. 51 | 52 | Follow [this instruction](https://slack.com/help/articles/115005265063-Incoming-Webhooks-for-Slack) to create Slack webhooks and obtain webhook URLs. 53 | 54 | If one would like a Slack notification pushed only if the newly registered API contains a specific tag, one should include the ```tag``` key, which should have the value of the specific tag (case sensitive). 55 | 56 | For example: 57 | ``` 58 | "tags": 'translator' # will send every time an API is registered with a 'translator' tag 59 | ``` 60 | 61 | 7. Optionally import some API data from a saved dump file. Contact us for the dump file. 62 | And replace the name of the file in the command with the backup file name. 63 | ``` 64 | import admin 65 | admin.restore("smartapi_oas3_backup_20200706.json") 66 | ``` 67 | 8. Run dev server 68 | ``` 69 | python index.py --debug 70 | ``` 71 | You should now able to access API dev server at http://localhost:8000 72 | 73 | Note: On windows with python 3.7, if you cannot install pycurl automatically, try downloading a pre-compiled binary and install manually from https://www.lfd.uci.edu/~gohlke/pythonlibs/ 74 | -------------------------------------------------------------------------------- /web-app/src/assets/img/api-editor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 64 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /web-app/src/components/SimpleNetwork.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 121 | -------------------------------------------------------------------------------- /web-app/src/assets/img/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 30 | 34 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /.github/scripts/check_backup.py: -------------------------------------------------------------------------------- 1 | """ 2 | This script checks if a backup file for the current date exists in a specified S3 bucket. 3 | If the backup file does not exist, a notification is sent to a Slack channel. 4 | 5 | Expected file format in the S3 bucket: 6 | - The file should be in the folder 'db_backup/' with the following naming pattern: 7 | 'smartapi_YYYYMMDD.zip', where YYYYMMDD corresponds to the current date. 8 | 9 | Required Environment Variables: 10 | - AWS_ACCESS_KEY_ID: The AWS access key ID to read the AWS s3 bucket. 11 | - AWS_SECRET_ACCESS_KEY: The AWS secret access key to read the AWS s3 bucket. 12 | - BACKUP_BUCKET_NAME: The name of the AWS S3 bucket where backups are stored. 13 | - S3_FOLDER: The folder path within the S3 bucket where backups are stored (e.g., 'db_backup/'). 14 | - AWS_REGION: The AWS region where the S3 bucket is located. 15 | - SLACK_CHANNEL: The Slack channel where notifications should be sent (e.g., '#observability-test'). 16 | - SLACK_WEBHOOK_URL: The Slack Webhook URL used to send the notification. 17 | 18 | Functionality: 19 | 1. The script uses the AWS SDK (boto3) to check for the existence of the backup file in the specified S3 bucket. 20 | 2. If the file is found, it logs that no action is needed. 21 | 3. If the file is not found, it sends a notification to the configured Slack channel. 22 | 23 | Dependencies: 24 | - boto3: For interacting with AWS S3. 25 | - requests: For sending HTTP POST requests to Slack. 26 | 27 | """ 28 | 29 | import boto3 30 | import botocore 31 | import os 32 | import requests 33 | 34 | from datetime import datetime 35 | 36 | 37 | def send_slack_notification(message): 38 | 39 | print(f" └─ {message}") 40 | 41 | # Create the payload for Slack 42 | slack_data = { 43 | "channel": os.getenv("SLACK_CHANNEL"), 44 | "username": "SmartAPI", 45 | "icon_emoji": ":thumbsdown:", 46 | "text": message, 47 | } 48 | 49 | try: 50 | print(" └─ Sending Slack notification.") 51 | response = requests.post(os.getenv("SLACK_WEBHOOK_URL"), json=slack_data, timeout=10) 52 | if response.status_code == 200: 53 | print(" └─ Slack notification sent successfully.") 54 | else: 55 | print(f" └─ Failed to send message to Slack: {response.status_code}, {response.text}") 56 | except requests.exceptions.Timeout as e: 57 | print(" └─ Request timed out to Slack WebHook URL.") 58 | raise e 59 | except requests.exceptions.RequestException as e: 60 | print(f" └─ Failed to send Slack notification. Error: {str(e)}") 61 | raise e 62 | 63 | 64 | def check_backup_file(): 65 | 66 | # Create the expected file name 67 | today_date = datetime.today().strftime("%Y%m%d") 68 | expected_file = f"{os.getenv('S3_FOLDER')}smartapi_{today_date}.zip" 69 | 70 | # Create the S3 client 71 | s3_client = boto3.client("s3", region_name=os.getenv("AWS_REGION")) 72 | 73 | # Try to fetch the file metadata 74 | try: 75 | response = s3_client.head_object(Bucket=os.getenv("BACKUP_BUCKET_NAME"), Key=expected_file) 76 | print(f" └─ Backup file {expected_file} exists!") 77 | 78 | # Get the file size in bytes 79 | file_size = response['ContentLength'] 80 | 81 | # Check if the file is larger than 1MB 82 | if file_size > 1048576: # 1MB in bytes 83 | print(f" └─ Backup file is larger than 1MB! Size: {file_size} bytes.") 84 | print(" └─ Nothing to do!") 85 | else: 86 | message = f":alert: The backup file {expected_file} is smaller than 1MB!" 87 | send_slack_notification(message) 88 | 89 | except botocore.exceptions.ClientError as e: 90 | print(e) 91 | message = f":alert: The backup file {expected_file} was NOT created today!" 92 | send_slack_notification(message) 93 | 94 | 95 | if __name__ == "__main__": 96 | check_backup_file() 97 | -------------------------------------------------------------------------------- /web-app/src/assets/img/whatis.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 40 | 41 | 42 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 66 | 67 | 68 | --------------------------------------------------------------------------------