├── .codecov.yml
├── .codespellrc
├── .eslintrc.cjs
├── .flake8
├── .github
└── workflows
│ ├── codespell.yml
│ ├── linter_checks.yml
│ ├── pypi_release.yaml
│ ├── tests.yml
│ └── vercel_deploy.yaml
├── .gitignore
├── .vercelignore
├── LICENSE
├── README.md
├── _vercel_dev.json
├── _vercel_prod.json
├── api
└── index.py
├── devel
└── troubleshoot_remote_read_speed.py
├── doc
├── about_the_api.md
├── compute_resource_prerequisites.md
├── create_dendro_app.md
├── for_developers.md
├── host_compute_resource.md
├── iac_aws_batch.md
└── letter_count
│ ├── .gitignore
│ ├── README.md
│ ├── input.txt
│ ├── main.py
│ ├── output.json
│ ├── sample_context_1.yaml
│ ├── sample_context_2.yaml
│ ├── sample_input.txt
│ ├── sample_output_1.json
│ ├── sample_output_2.json
│ └── spec.json
├── iac
└── aws_batch
│ ├── .gitignore
│ ├── README.md
│ ├── __init__.py
│ ├── app.py
│ ├── aws_batch
│ ├── __init__.py
│ ├── aws_batch_stack.py
│ └── stack_config.py
│ ├── cdk.json
│ ├── devel
│ └── create_ami_map.py
│ ├── requirements-dev.txt
│ ├── requirements.txt
│ ├── source.bat
│ └── tests
│ ├── __init__.py
│ └── unit
│ ├── __init__.py
│ └── test_aws_batch_stack.py
├── index.html
├── package.json
├── public
├── dendro.png
├── help
│ ├── common.md
│ ├── dandiset.md
│ ├── dandisets.md
│ ├── home.md
│ ├── project-dandi-import.md
│ ├── project-processors.md
│ ├── project-project-files.md
│ ├── project-project-home.md
│ ├── project-project-jobs.md
│ └── project-project-linked-analysis.md
└── scripts
│ ├── load_electrical_series.py
│ └── load_spike_sorting.py
├── python
├── .gitignore
├── README.md
├── dendro
│ ├── __init__.py
│ ├── api_helpers
│ │ ├── __init__.py
│ │ ├── clients
│ │ │ ├── MockMongoClient.py
│ │ │ ├── __init__.py
│ │ │ ├── _get_mongo_client.py
│ │ │ ├── _remove_id_field.py
│ │ │ ├── db.py
│ │ │ └── pubsub.py
│ │ ├── core
│ │ │ ├── __init__.py
│ │ │ ├── _create_random_id.py
│ │ │ ├── _get_project_role.py
│ │ │ ├── _hide_secret_params_in_job.py
│ │ │ ├── _model_dump.py
│ │ │ └── settings.py
│ │ ├── routers
│ │ │ ├── __init__.py
│ │ │ ├── client
│ │ │ │ ├── __init__.py
│ │ │ │ └── router.py
│ │ │ ├── common.py
│ │ │ ├── compute_resource
│ │ │ │ ├── __init__.py
│ │ │ │ └── router.py
│ │ │ ├── gui
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _authenticate_gui_request.py
│ │ │ │ ├── compute_resource_routes.py
│ │ │ │ ├── create_job_route.py
│ │ │ │ ├── file_routes.py
│ │ │ │ ├── find_routes.py
│ │ │ │ ├── github_auth_routes.py
│ │ │ │ ├── job_routes.py
│ │ │ │ ├── project_routes.py
│ │ │ │ ├── router.py
│ │ │ │ ├── script_routes.py
│ │ │ │ ├── usage_routes.py
│ │ │ │ └── user_routes.py
│ │ │ └── processor
│ │ │ │ ├── __init__.py
│ │ │ │ └── router.py
│ │ └── services
│ │ │ ├── __init__.py
│ │ │ ├── _create_output_file.py
│ │ │ ├── _crypto_keys.py
│ │ │ ├── _remove_detached_files_and_jobs.py
│ │ │ ├── gui
│ │ │ ├── __init__.py
│ │ │ ├── create_job.py
│ │ │ ├── delete_project.py
│ │ │ ├── get_compute_resource_user_usage.py
│ │ │ └── set_file.py
│ │ │ └── processor
│ │ │ ├── __init__.py
│ │ │ ├── _get_fsbucket_signed_upload_url.py
│ │ │ ├── _get_signed_upload_url.py
│ │ │ ├── get_upload_url.py
│ │ │ └── update_job_status.py
│ ├── aws_batch
│ │ ├── __init__.py
│ │ └── aws_batch_job_definition.py
│ ├── cli.py
│ ├── client
│ │ ├── Project.py
│ │ ├── __init__.py
│ │ ├── _create_batch_id.py
│ │ ├── _interim
│ │ │ ├── NwbRecording.py
│ │ │ ├── NwbSorting.py
│ │ │ └── __init__.py
│ │ ├── _upload_blob.py
│ │ ├── set_file.py
│ │ └── submit_job.py
│ ├── common
│ │ ├── __init__.py
│ │ ├── _api_request.py
│ │ ├── _crypto_keys.py
│ │ └── dendro_types.py
│ ├── compute_resource
│ │ ├── AppManager.py
│ │ ├── ComputeResourceException.py
│ │ ├── JobManager.py
│ │ ├── PubsubClient.py
│ │ ├── SlurmJobHandler.py
│ │ ├── __init__.py
│ │ ├── _run_job_in_aws_batch.py
│ │ ├── _start_job.py
│ │ ├── register_compute_resource.py
│ │ └── start_compute_resource.py
│ ├── internal_job_monitoring
│ │ ├── __init__.py
│ │ ├── common.py
│ │ ├── console_output_monitor.py
│ │ ├── job_status_monitor.py
│ │ └── resource_utilization_monitor.py
│ ├── mock.py
│ ├── sdk
│ │ ├── App.py
│ │ ├── AppProcessor.py
│ │ ├── FileManifest.py
│ │ ├── InputFile.py
│ │ ├── InputFolder.py
│ │ ├── Job.py
│ │ ├── OutputFile.py
│ │ ├── OutputFolder.py
│ │ ├── ProcessorBase.py
│ │ ├── __init__.py
│ │ ├── _load_spec_from_uri.py
│ │ ├── _make_spec_file.py
│ │ ├── _run_job_child_process.py
│ │ ├── _run_job_parent_process.py
│ │ ├── _test_app_processor.py
│ │ └── get_project_file_from_uri.py
│ └── version.txt
├── mock-output-file.txt
├── pytest.ini
├── setup.py
└── tests
│ ├── mock_app
│ └── main.py
│ ├── mock_app_2
│ └── main.py
│ ├── test_api_request_failures.py
│ ├── test_client.py
│ ├── test_compute_resource.py
│ ├── test_crypto_keys.py
│ ├── test_github_auth_route.py
│ ├── test_integration.py
│ ├── test_misc.py
│ └── test_sdk.py
├── requirements.txt
├── src
├── ApiKeysWindow
│ └── ApiKeysWindow.tsx
├── App.css
├── App.tsx
├── ApplicationBar.tsx
├── ComputeResourceNameDisplay.tsx
├── DendroContext
│ └── DendroContext.tsx
├── GitHub
│ ├── GitHubAuthPage.tsx
│ ├── GitHubLoginWindow.tsx
│ ├── GitHubLoginWindowOld.tsx
│ └── PersonalAccessTokenWindow.tsx
├── GithubAuth
│ ├── GithubAuthContext.ts
│ ├── GithubAuthSetup.tsx
│ ├── getGithubAuthFromLocalStorage.ts
│ ├── useGithubAuth.ts
│ └── useSetupGithubAuth.ts
├── HelpPanel
│ └── HelpPanel.tsx
├── MainWindow.tsx
├── Markdown
│ └── Markdown.tsx
├── RecentProjectsPanel
│ └── RecentProjectsPanel.tsx
├── RemoteH5File
│ ├── RemoteH5File.ts
│ ├── RemoteH5Worker.js
│ ├── h5wasm
│ │ ├── file_handlers.js
│ │ ├── h5wasm_license.txt
│ │ ├── hdf5_hl.js
│ │ ├── hdf5_util_jfm.js
│ │ ├── readme.txt
│ │ └── wasmBinaryFile.js
│ └── helpers.ts
├── TabWidget
│ ├── TabWidget.tsx
│ └── TabWidgetTabBar.tsx
├── UserIdComponent.tsx
├── confirm_prompt_alert.ts
├── dbInterface
│ ├── dbInterface.ts
│ └── getAuthorizationHeaderForUrl.ts
├── index.css
├── main.tsx
├── nh5.ts
├── pages
│ ├── AboutPage
│ │ └── AboutPage.tsx
│ ├── AdminPage
│ │ └── AdminPage.tsx
│ ├── ComputeResourcePage
│ │ ├── ComputeResourceAppsTable.tsx
│ │ ├── ComputeResourceAppsTableMenuBar.tsx
│ │ ├── ComputeResourcePage.tsx
│ │ └── NewAppWindow.tsx
│ ├── ComputeResourcesPage
│ │ ├── ComputeResourcesContext.tsx
│ │ ├── ComputeResourcesPage.css
│ │ ├── ComputeResourcesPage.tsx
│ │ └── ComputeResourcesTable.tsx
│ ├── DandiBrowser
│ │ ├── DandiBrowser.tsx
│ │ ├── DandisetView.tsx
│ │ ├── SearchResults.tsx
│ │ ├── formatByteCount.ts
│ │ ├── types.ts
│ │ └── useProjectsForTag.ts
│ ├── HomePage
│ │ ├── HomePage.css
│ │ └── HomePage.tsx
│ ├── ImportDandiAssetPage
│ │ └── ImportDandiAssetPage.tsx
│ ├── ProjectPage
│ │ ├── ComputeResourceAppsTable2
│ │ │ ├── ComputeResourceAppsTable2.tsx
│ │ │ └── ComputeResourceAppsTableMenuBar2.tsx
│ │ ├── ComputeResourceSection.tsx
│ │ ├── ComputeResourceUsageComponent
│ │ │ └── ComputeResourceUsageComponent.tsx
│ │ ├── DandiUpload
│ │ │ ├── DandiUploadWindow.tsx
│ │ │ └── prepareDandiUploadTask.ts
│ │ ├── EditJobDefinitionWindow
│ │ │ └── EditJobDefinitionWindow.tsx
│ │ ├── FileActions.tsx
│ │ ├── FileBrowser
│ │ │ ├── DropdownMenu.css
│ │ │ ├── DropdownMenu.tsx
│ │ │ ├── FileBrowser2.tsx
│ │ │ ├── FileBrowserMenuBar.tsx
│ │ │ ├── FileBrowserTable.tsx
│ │ │ ├── file-browser-table.css
│ │ │ └── formatByteCount.ts
│ │ ├── FileView
│ │ │ ├── ElectricalSeriesSection
│ │ │ │ ├── ElectricalSeriesSection.tsx
│ │ │ │ └── LoadElectricalSeriesScriptWindow.tsx
│ │ │ ├── FigurlFileView.tsx
│ │ │ ├── FileView.tsx
│ │ │ ├── FileViewTable.tsx
│ │ │ ├── FolderFileView.tsx
│ │ │ ├── Nh5FileView.tsx
│ │ │ ├── NwbFileView.tsx
│ │ │ ├── OtherFileView.tsx
│ │ │ └── SpikeSortingOutputSection
│ │ │ │ ├── LoadSpikeSortingInScriptWindow.tsx
│ │ │ │ └── SpikeSortingOutputSection.tsx
│ │ ├── GenerateSpikeSortingSummaryWindow
│ │ │ └── GenerateSpikeSortingSummaryWindow.tsx
│ │ ├── JobView
│ │ │ ├── JobView.tsx
│ │ │ └── OutputsTable.tsx
│ │ ├── JobsWindow
│ │ │ ├── JobsTable.tsx
│ │ │ ├── JobsTableMenuBar.tsx
│ │ │ └── JobsWindow.tsx
│ │ ├── LoadNwbInPythonWindow
│ │ │ └── LoadNwbInPythonWindow.tsx
│ │ ├── MearecGenerateTemplatesWindow
│ │ │ └── MearecGenerateTemplatesWindow.tsx
│ │ ├── Processor.css
│ │ ├── ProcessorsView.tsx
│ │ ├── ProjectAnalysis
│ │ │ ├── AnalysisSourceClient.ts
│ │ │ ├── AnalysisSourceFileBrowser.tsx
│ │ │ ├── AnalysisSourceFileView.css
│ │ │ ├── AnalysisSourceFileView.tsx
│ │ │ ├── ClonedRepo.ts
│ │ │ ├── ProjectAnalysis.tsx
│ │ │ └── fs.ts
│ │ ├── ProjectFiles.tsx
│ │ ├── ProjectHome.tsx
│ │ ├── ProjectJobs.tsx
│ │ ├── ProjectPage.tsx
│ │ ├── ProjectPageContext.tsx
│ │ ├── ProjectScripts.tsx
│ │ ├── ProjectSettingsWindow.tsx
│ │ ├── ResourceUtilizationView
│ │ │ ├── LogPlot.tsx
│ │ │ └── ResourceUtilizationView.tsx
│ │ ├── RunBatchSpikeSortingWindow
│ │ │ ├── ElectrodeGeometryView.tsx
│ │ │ ├── ElectrodeGeometryWidget.tsx
│ │ │ ├── LeftColumn.tsx
│ │ │ ├── RequiredResourcesEditor.tsx
│ │ │ ├── RightColumn.tsx
│ │ │ ├── RunBatchSpikeSortingWindow.tsx
│ │ │ ├── SelectProcessorComponent.tsx
│ │ │ └── getDefaultRequiredResources.ts
│ │ ├── RunFileActionWindow
│ │ │ └── RunFileActionWindow.tsx
│ │ ├── UploadSmalFileWindow
│ │ │ └── UploadSmallFileWindow.tsx
│ │ ├── openFilesInNeurosift.ts
│ │ └── scripts
│ │ │ ├── CodeEditor.tsx
│ │ │ ├── RunScript
│ │ │ ├── RunScriptWindow.tsx
│ │ │ ├── RunScriptWorkerTypes.ts
│ │ │ └── runScriptWorker.ts
│ │ │ ├── ScriptView.tsx
│ │ │ ├── ScriptsTable.tsx
│ │ │ └── ScriptsTableMenuBar.tsx
│ ├── ProjectsPage
│ │ ├── ProjectsPage.css
│ │ ├── ProjectsPage.tsx
│ │ ├── ProjectsTable.tsx
│ │ └── useProjectsForUser.ts
│ └── RegisterComputeResourcePage
│ │ └── RegisterComputeResourcePage.tsx
├── plugins
│ ├── DendroFrontendPlugin.ts
│ ├── initializePlugins.ts
│ └── mearec
│ │ └── mearecPlugin.ts
├── pubnub
│ └── pubnub.ts
├── scientific-table.css
├── table1.css
├── timeStrings.ts
├── types
│ ├── dendro-types.ts
│ └── validateObject.ts
├── useRoute.ts
├── useWindowDimensions.ts
└── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
├── vercel.json
├── vercel.json.readme.md
├── vite.config.ts
└── yarn.lock
/.codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | project:
4 | default:
5 | informational: true
6 | patch:
7 | default:
8 | informational: true
--------------------------------------------------------------------------------
/.codespellrc:
--------------------------------------------------------------------------------
1 | [codespell]
2 | skip = .git,*.pdf,*.svg,*.lock,*.css,.codespellrc,input.txt
3 | check-hidden = true
4 | # ignore-regex =
5 | ignore-words-list = te,nd
6 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:react/recommended",
9 | "plugin:@typescript-eslint/recommended",
10 | "plugin:react-hooks/recommended"
11 | ],
12 | "overrides": [
13 | ],
14 | "parser": "@typescript-eslint/parser",
15 | "parserOptions": {
16 | "ecmaVersion": "latest",
17 | "sourceType": "module"
18 | },
19 | "plugins": [
20 | "react",
21 | "@typescript-eslint",
22 | "react-hooks"
23 | ],
24 | "rules": {
25 | "react/react-in-jsx-scope": "off",
26 | "@typescript-eslint/no-explicit-any": "off",
27 | "@typescript-eslint/no-empty-function": "off"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | extend-ignore = E124,E128,E301,E302,E305,E402,E501,E261,W504
3 | # E124: closing bracket does not match visual indentation
4 | # E128: continuation line under-indented for visual indent
5 | # E301: expected 1 blank line, found 0
6 | # E302: expected 2 blank lines, found 1
7 | # E305: expected 2 blank lines after class or function definition, found 1
8 | # E402: module level import not at top of file
9 | # E501: line too long (82 > 79 characters)
10 | # E261: at least two spaces before inline comment
11 | # W504: line break after binary operator
--------------------------------------------------------------------------------
/.github/workflows/codespell.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Codespell
3 |
4 | on:
5 | push:
6 | pull_request:
7 |
8 | permissions:
9 | contents: read
10 |
11 | jobs:
12 | codespell:
13 | name: Check for spelling errors
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v3
19 | - name: Codespell
20 | uses: codespell-project/actions-codespell@v2
21 |
--------------------------------------------------------------------------------
/.github/workflows/linter_checks.yml:
--------------------------------------------------------------------------------
1 | name: testing
2 |
3 | on:
4 | push:
5 | paths:
6 | - 'python/**'
7 | pull_request:
8 | paths:
9 | - 'python/**'
10 | workflow_dispatch:
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | name: Linter checks
16 | steps:
17 | - uses: actions/checkout@v1
18 | - name: Install
19 | run: cd python && pip install -e .[compute_resource]
20 | - name: Install packages needed for tests
21 | run: pip install pytest pytest-asyncio pytest-cov pyright boto3 kachery_cloud flake8
22 | - name: Install additional packages used by api_helpers
23 | run: cd python && pip install -e .[api]
24 | - name: Install additional optional packages
25 | run: pip install GPUtil
26 | # - name: Install packages needed by iac/aws_batch
27 | # run: cd iac/aws_batch && pip install -r requirements.txt
28 | - name: Run linter checks
29 | run: cd python && flake8 --config ../.flake8 && pyright
30 | # - name: Run linter checks for iac
31 | # run: cd iac && flake8 --config ../.flake8 && pyright
32 |
--------------------------------------------------------------------------------
/.github/workflows/pypi_release.yaml:
--------------------------------------------------------------------------------
1 | name: PyPI release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | # only run on changes to python/dendro/version.txt
9 | - python/dendro/version.txt
10 | # manual trigger
11 | workflow_dispatch:
12 |
13 | jobs:
14 | pypi-release:
15 | name: PyPI release
16 | runs-on: ubuntu-latest
17 | steps:
18 | -
19 | uses: actions/checkout@v3
20 | -
21 | name: Set up Python
22 | uses: actions/setup-python@v3
23 | with:
24 | python-version: "3.10"
25 | -
26 | name: Install
27 | run: cd python && pip install -e .[compute_resource]
28 | -
29 | name: Install packages needed for tests
30 | run: pip install pytest pytest-asyncio pytest-cov boto3 kachery_cloud
31 | -
32 | name: Run non-api tests
33 | run: cd python && pytest -m "not api" tests/ # make sure we are not depending on any of the additional packages in requirements.txt
34 | -
35 | name: Install packages needed for api tests
36 | run: cd python && pip install -e .[api]
37 | -
38 | name: Install other packages needed for api tests
39 | run: pip install httpx
40 | -
41 | name: Run tests and collect coverage
42 | run: cd python && pytest tests/
43 | -
44 | name: Install dependencies
45 | run: |
46 | python -m pip install --upgrade pip
47 | pip install build
48 | pip install twine
49 | -
50 | name: Build and publish to PyPI
51 | run: |
52 | cd python
53 | python -m build
54 | twine upload dist/*
55 | env:
56 | TWINE_USERNAME: __token__
57 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
58 | -
59 | name: Tag the release using version from python/dendro/version.txt
60 | run: |
61 | git config --global user.email "jmagland@flatironinstitute.org"
62 | git config --global user.name "Jeremy Magland"
63 | git tag -a v$(cat python/dendro/version.txt | tr -d '[:space:]') -m "v$(cat python/dendro/version.txt | tr -d '[:space:]')"
64 | git push origin --tags
65 | env:
66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
67 |
68 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | paths:
6 | - 'python/**'
7 | pull_request:
8 | paths:
9 | - 'python/**'
10 | workflow_dispatch:
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | name: Tests
16 | steps:
17 | - uses: actions/checkout@v1
18 |
19 | # Install dendro and packages needed for tests
20 | - name: Install
21 | run: cd python && pip install -e .[compute_resource]
22 | - name: Install packages needed for tests
23 | run: pip install pytest pytest-asyncio pytest-cov boto3 kachery_cloud
24 |
25 | # Run non-api tests
26 | - name: Run non-api tests
27 | run: cd python && pytest -m "not api" tests/ # make sure we are not depending on any of the additional packages in requirements.txt
28 | - name: Install packages needed for api tests
29 |
30 | # Install packages needed for api tests
31 | run: cd python && pip install -e .[api]
32 | - name: Install other packages needed for api tests
33 | run: pip install httpx
34 |
35 | # Run full tests and collect coverage
36 | - name: Run tests and collect coverage
37 | run: cd python && pytest --cov dendro --cov-report=xml --cov-report=term tests/
38 |
39 | # Try with pydantic v1 (no coverage this time)
40 | - name: Try with pydantic v1
41 | run: pip install pydantic==1.9.2 # support versions >= 1.9.2
42 | - name: Run tests
43 | run: cd python && pytest tests/
44 |
45 | - uses: codecov/codecov-action@v3
46 | with:
47 | token: ${{ secrets.CODECOV_TOKEN }}
48 | fail_ci_if_error: false
49 | file: ./python/coverage.xml
50 | flags: unittests
--------------------------------------------------------------------------------
/.github/workflows/vercel_deploy.yaml:
--------------------------------------------------------------------------------
1 | name: Vercel deploy
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | # only run workflow when changes are made to these files
9 | - package.json
10 | # manual trigger
11 | workflow_dispatch:
12 |
13 | jobs:
14 | vercel-deploy:
15 | name: Vercel deploy
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v2
19 | -
20 | name: Install Vercel CLI
21 | run: npm install --global vercel@latest
22 | -
23 | name: Set vercel.json for production
24 | run: cp _vercel_prod.json vercel.json
25 | -
26 | # Not sure why this is needed, but it is
27 | name: Create .vercel/project.json
28 | run: |
29 | mkdir -p .vercel
30 | echo '{"projectId":"${{ secrets.VERCEL_PROJECT_ID }}", "orgId":"${{ secrets.VERCEL_ORG_ID }}"}' > .vercel/project.json
31 | -
32 | name: Pull Vercel Environment Information
33 | run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
34 | -
35 | name: Build Project Artifacts
36 | run: vercel --prod --token=${{ secrets.VERCEL_TOKEN }}
37 |
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | slurm_batch_*
2 | slurm_group_assignments
3 | tmp
4 | __pycache__
5 |
6 | # Logs
7 | logs
8 | *.log
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | pnpm-debug.log*
13 | lerna-debug.log*
14 |
15 | node_modules
16 | dist
17 | dist-ssr
18 | *.local
19 |
20 | # Editor directories and files
21 | .vscode/*
22 | !.vscode/extensions.json
23 | .idea
24 | .DS_Store
25 | *.suo
26 | *.ntvs*
27 | *.njsproj
28 | *.sln
29 | *.sw?
30 | .vercel
31 |
--------------------------------------------------------------------------------
/.vercelignore:
--------------------------------------------------------------------------------
1 | __pycache__
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://badge.fury.io/py/dendro)
2 | [](https://github.com/flatironinstitute/dendro/actions/workflows/tests.yml)
3 | [](https://codecov.io/gh/flatironinstitute/dendro)
4 |
5 | NOTE: [Here is the more recent version of Dendro](https://github.com/magland/dendro)
6 |
7 | # BELOW is OLD/OBSOLETE information
8 |
9 | ## Getting started
10 |
11 | [View the documentation](https://flatironinstitute.github.io/dendro-docs)
12 |
13 | [Visit the live site](https://dendro.vercel.app/)
14 |
15 | ### LICENSE
16 |
17 | Apache 2.0
18 |
19 | ## Authors
20 |
21 | Jeremy Magland (Flatiron Institute) and Luiz Tauffer (CatalystNeuro)
22 |
23 | In collaboration with Ben Dichter (CatalystNeuro)
24 |
25 |
--------------------------------------------------------------------------------
/_vercel_dev.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | {
4 | "source": "/api/(.*)",
5 | "destination": "/api/index.py"
6 | },
7 | {
8 | "source": "/docs",
9 | "destination": "/api/index.py"
10 | },
11 | {
12 | "source": "/redoc",
13 | "destination": "/api/index.py"
14 | },
15 | {
16 | "source": "/openapi.json",
17 | "destination": "/api/index.py"
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/_vercel_prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | {
4 | "source": "/api/(.*)",
5 | "destination": "/api/index.py"
6 | },
7 | {
8 | "source": "/docs",
9 | "destination": "/api/index.py"
10 | },
11 | {
12 | "source": "/redoc",
13 | "destination": "/api/index.py"
14 | },
15 | {
16 | "source": "/openapi.json",
17 | "destination": "/api/index.py"
18 | },
19 | {
20 | "source": "/(.*)",
21 | "destination": "/index.html"
22 | }
23 | ]
24 | }
--------------------------------------------------------------------------------
/api/index.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI
2 |
3 | # Here's the reason that all the other Python files are in ../python/dendro/api_helpers
4 | # I was noticing very long build times (~15 minutes)...
5 | # Apparently, vercel treats every .py file in /api as a lambda function.
6 | # So it was building each and every one of them, even though index.py should be the only one.
7 | # See https://github.com/orgs/vercel/discussions/46
8 |
9 | import os
10 | thisdir = os.path.dirname(os.path.realpath(__file__))
11 |
12 | import sys
13 | print(f'This dir: {thisdir}')
14 | sys.path.append(thisdir + "/../python")
15 | from dendro.api_helpers.routers.processor.router import router as processor_router
16 | from dendro.api_helpers.routers.compute_resource.router import router as compute_resource_router
17 | from dendro.api_helpers.routers.client.router import router as client_router
18 | from dendro.api_helpers.routers.gui.router import router as gui_router
19 |
20 | from fastapi.middleware.cors import CORSMiddleware
21 |
22 |
23 | app = FastAPI()
24 |
25 | # Set up CORS
26 | origins = [
27 | "http://localhost:3000",
28 | "http://localhost:3001",
29 | "http://localhost:5173",
30 | "http://localhost:4200",
31 | "https://dendro.vercel.app",
32 | "https://flatironinstitute.github.io",
33 | "https://neurosift.app",
34 | "https://dendro-arc.vercel.app"
35 | ]
36 |
37 | app.add_middleware(
38 | CORSMiddleware,
39 | allow_origins=origins,
40 | allow_credentials=True,
41 | allow_methods=["*"],
42 | allow_headers=["*"],
43 | )
44 |
45 | # requests from a processing job
46 | app.include_router(processor_router, prefix="/api/processor", tags=["Processor"])
47 |
48 | # requests from a compute resource
49 | app.include_router(compute_resource_router, prefix="/api/compute_resource", tags=["Compute Resource"])
50 |
51 | # requests from a client (usually Python)
52 | app.include_router(client_router, prefix="/api/client", tags=["Client"])
53 |
54 | # requests from the GUI
55 | app.include_router(gui_router, prefix="/api/gui", tags=["GUI"])
56 |
--------------------------------------------------------------------------------
/devel/troubleshoot_remote_read_speed.py:
--------------------------------------------------------------------------------
1 | import time
2 | import h5py
3 | import remfile
4 |
5 | url = 'https://dandiarchive-embargo.s3.amazonaws.com/000620/blobs/8ce/bee/8cebeede-f6e8-4bd5-a307-ed3c852269bb?response-content-disposition=attachment%3B%20filename%3D%22sub-Elgar_ecephys.nwb%22&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAUBRWC5GAEKH3223E%2F20231101%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20231101T153921Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=0f41fc34bb4c1759489fca53c439359bfb450efac1f1780672c480a1d127569c'
6 |
7 | # open the remote file
8 | f = h5py.File(remfile.File(url, verbose=True), 'r')
9 |
10 | # load the neurodata object
11 | X = f['/acquisition/ElectricalSeriesAP']
12 |
13 | starting_time = X['starting_time'][()]
14 | rate = X['starting_time'].attrs['rate']
15 | data = X['data']
16 |
17 | print(f'starting_time: {starting_time}')
18 | print(f'rate: {rate}')
19 | print(f'data shape: {data.shape}')
20 |
21 | timer = time.time()
22 | x = data[:30000 * 60,0:10]
23 | elapsed = time.time() - timer
24 | print(f'elapsed (sec): {elapsed}')
25 |
26 | print(x.shape)
--------------------------------------------------------------------------------
/doc/about_the_api.md:
--------------------------------------------------------------------------------
1 | # About the Dendro API
2 |
3 | The Dendro API is implemented using Python with [FastAPI](https://fastapi.tiangolo.com/), deployed as serverless functions on [Vercel](https://vercel.com/about). It is divided into four sections: GUI, Compute Resource, Processor, and Client.
4 |
5 | Here is the [ReDoc-generated documentation](https://dendro.vercel.app/redoc) and the [Swagger UI-generated documentation](https://dendro.vercel.app/docs).
6 |
7 | Here is the source code: [index.py](../api/index.py) and [api_helpers](../python/dendro/api_helpers).
8 |
9 |
10 | ## GUI
11 |
12 | The GUI API receives requests from the [web interface](https://dendro.vercel.app). Operations include
13 |
14 | * Getting and creating projects
15 | * Getting and creating project files
16 | * Getting and creating project jobs
17 | * Registering and managing compute resources, including configuring apps.
18 | * Getting pub/sub information
19 | * Authenticating via GitHub
20 |
21 | All write operations and some read operations require authentication via GitHub.
22 |
23 | ## Compute Resource
24 |
25 | The compute resource API receives requests from compute resources. Operations include
26 |
27 | * Getting the list of apps configured on the web interface
28 | * Getting pub/sub information
29 | * Getting a list of unfinished jobs for the compute resource, including their private keys
30 | * Setting the compute resource spec (on startup of the compute resource)
31 |
32 | All operations require authentication via signatures created using the secret compute resource private key, which is associated with the public compute resource ID.
33 |
34 | ## Processor
35 |
36 | The processor API receives requests from processing jobs. Operations include
37 |
38 | * Getting detailed information about a processing job
39 | * Getting the status of a processing job (to see if it has been canceled)
40 | * Setting the status of a processing job (running, completed, failed, ...)
41 | * Getting presigned upload URLs for uploading outputs and console logs
42 |
43 | All operations are authenticated using the job private key
44 |
45 | ## Client
46 |
47 | The client API receives requests from Python clients. Operations include
48 |
49 | * Getting the files and jobs associated with a project
50 |
51 | At this time, no authentication is required.
52 |
--------------------------------------------------------------------------------
/doc/compute_resource_prerequisites.md:
--------------------------------------------------------------------------------
1 | # Dendro compute resource prerequisites
2 |
3 | Suppose you have a Linux machine and would like to use it as a dendro compute resource. Here are the recommended steps to prepare it with the necessary software.
4 |
5 | ## Use Linux
6 |
7 | Any distribution should do.
8 |
9 | ## Conda / Miniconda
10 |
11 | I recommend using Miniconda, but you can use other conda solutions, virtualenv, etc.
12 |
13 | Follow these instructions: https://docs.conda.io/projects/miniconda/en/latest/miniconda-install.html
14 |
15 | This will involve downloading the Linux installation script file (usually the "Miniconda3 Linux 64-bit" option) and running it using bash.
16 |
17 | Create a new conda environment. I'd recommend using Python 3.9, but you can more recent versions as well.
18 |
19 | ```bash
20 | conda create -n processing python=3.9
21 | ```
22 |
23 | You can replace "processing" with any name you like. To use this environment run the following each time you open your terminal
24 |
25 | ```bash
26 | conda activate processing
27 | ```
28 |
29 | or add this command to your ~/.bashrc file to automatically start in this environment each time you open a new terminal.
30 |
31 | ## Install docker or apptainer
32 |
33 | To use your computer as a dendro compute resource, you'll most likely need to install either docker or apptainer (or singularity). Docker is simpler to install, whereas apptainer is better for shared environments or compute clusters.
34 |
35 | To install docker server (or docker engine): https://docs.docker.com/engine/install/
36 |
37 | To get started with apptainer: https://apptainer.org/
--------------------------------------------------------------------------------
/doc/host_compute_resource.md:
--------------------------------------------------------------------------------
1 | # Hosting a Dendro compute resource
2 |
3 | Each Dendro project comes equipped with a dedicated compute resource for executing analysis jobs. The default setting uses a compute resource provided by the authors with limitations on CPU, memory, and concurrent jobs, shared among all users. This public resource should only be used for testing with small jobs. Contact one of the authors if you would like to run more intensive processing or configure your own compute resources.
4 |
5 | Prerequisites
6 |
7 | * Python >= 3.9
8 | * Docker or apptainer (or singularity >= 3.11)
9 |
10 | Clone this repo, then
11 |
12 | ```bash
13 | # install
14 | cd dendro/python
15 | pip install -e .[compute_resource]
16 | ```
17 |
18 | ```bash
19 | # Initialize (one time)
20 | export COMPUTE_RESOURCE_DIR=/some/path
21 | export CONTAINER_METHOD=apptainer # or docker (or singularity)
22 | cd $COMPUTE_RESOURCE_DIR
23 | dendro register-compute-resource
24 | # Open the provided link in a browser and log in using GitHub
25 | ```
26 |
27 | ```bash
28 | # Start the compute resource
29 | cd $COMPUTE_RESOURCE_DIR
30 | dendro start-compute-resource
31 | # Leave this open in a terminal. It is recommended that you use a terminal multiplexer like tmux or screen.
32 | ```
33 |
34 | In the web interface, go to settings for your project, and select your compute resource. New jobs submitted within your project will now use your compute resource for analysis jobs.
35 |
36 | ## Configuring apps for your compute resource
37 |
38 | In order to run jobs with your compute resource, you will need to configure apps to use it.
39 |
40 | In the web interface, click on the appropriate link to manage your compute resource. You will then be able to add apps to your compute resource by entering the information (see below for available apps).
41 |
42 | :warning: After you make changes to your compute resource on the web interface, reload the page so that your changes will take effect.
43 |
44 | The following are available apps that you can configure
45 |
46 | | App name | Spec URI |
47 | | -------- | --------------- |
48 | | mountainsort5 | https://github.com/scratchrealm/pc-spike-sorting/blob/main/mountainsort5/spec.json |
49 | | kilosort3 | https://github.com/scratchrealm/pc-spike-sorting/blob/main/kilosort3/spec.json |
50 | | kilosort2_5 | https://github.com/scratchrealm/pc-spike-sorting/blob/main/kilosort2_5/spec.json |
51 | | spike-sorting_utils | https://github.com/scratchrealm/pc-spike-sorting/blob/main/spike_sorting_utils/spec.json |
52 | | dandi-upload | https://github.com/scratchrealm/pc-spike-sorting/blob/main/dandi_upload/spec.json |
53 |
54 | ## Submitting jobs to AWS Batch
55 |
56 | [See iac_aws_batch](./iac_aws_batch.md)
--------------------------------------------------------------------------------
/doc/letter_count/.gitignore:
--------------------------------------------------------------------------------
1 | input.txt
2 | output.json
--------------------------------------------------------------------------------
/doc/letter_count/README.md:
--------------------------------------------------------------------------------
1 | # Dendro app: letter_count
2 |
3 | This is an example Dendro processing app for the tutorial.
--------------------------------------------------------------------------------
/doc/letter_count/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import os
4 | import json
5 | from dendro.sdk import App, BaseModel, Field, ProcessorBase, InputFile, OutputFile
6 |
7 |
8 | app = App(
9 | 'letter_count',
10 | description="Example Dendro processing app for tutorial",
11 | app_image="",
12 | app_executable=os.path.abspath(__file__)
13 | )
14 |
15 | description = """
16 | This is the a processor in the letter_count app. It counts the number of times a particular letter appears in a text file and produces and JSON file with the result.
17 | """
18 |
19 | class LetterCountProcessorContext(BaseModel):
20 | input: InputFile = Field(description='Input text file')
21 | output: OutputFile = Field(description='Output JSON file')
22 | letter: str = Field(description='Letter to count')
23 |
24 | class LetterCountProcessor(ProcessorBase):
25 | name = 'letter_count'
26 | label = 'Letter Count'
27 | description = description
28 | tags = ['tutorial']
29 | attributes = {'wip': True}
30 |
31 | @staticmethod
32 | def run(context: LetterCountProcessorContext):
33 | input_fname = 'input.txt'
34 | output_fname = 'output.json'
35 | context.input.download(input_fname)
36 | letter = context.letter
37 |
38 | with open(input_fname, 'r') as f:
39 | text = f.read()
40 | count = text.count(letter)
41 |
42 | output = {
43 | 'count': count
44 | }
45 | with open(output_fname, 'w') as f:
46 | f.write(json.dumps(output))
47 | context.output.upload(output_fname)
48 |
49 | app.add_processor(LetterCountProcessor)
50 |
51 | if __name__ == '__main__':
52 | app.run()
53 |
--------------------------------------------------------------------------------
/doc/letter_count/output.json:
--------------------------------------------------------------------------------
1 | {"count": 110}
--------------------------------------------------------------------------------
/doc/letter_count/sample_context_1.yaml:
--------------------------------------------------------------------------------
1 | input:
2 | url: https://filesamples.com/samples/document/txt/sample3.txt
3 | output:
4 | output_file_name: sample_output_1.json
5 | letter: "d"
--------------------------------------------------------------------------------
/doc/letter_count/sample_context_2.yaml:
--------------------------------------------------------------------------------
1 | input:
2 | local_file_name: sample_input.txt
3 | output:
4 | output_file_name: sample_output_2.json
5 | letter: "d"
--------------------------------------------------------------------------------
/doc/letter_count/sample_input.txt:
--------------------------------------------------------------------------------
1 | This is a sample input file.
2 | Here are 7 letters: ddddddd
--------------------------------------------------------------------------------
/doc/letter_count/sample_output_1.json:
--------------------------------------------------------------------------------
1 | {"count": 110}
--------------------------------------------------------------------------------
/doc/letter_count/sample_output_2.json:
--------------------------------------------------------------------------------
1 | {"count": 7}
--------------------------------------------------------------------------------
/doc/letter_count/spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "letter_count",
3 | "description": "Example Dendro processing app for tutorial",
4 | "appImage": "",
5 | "appExecutable": "/home/magland/src/dendro/doc/letter_count/main.py",
6 | "executable": "/home/magland/src/dendro/doc/letter_count/main.py",
7 | "processors": [
8 | {
9 | "name": "letter_count",
10 | "description": "\nThis is the a processor in the letter_count app. It counts the number of times a particular letter appears in a text file and produces and JSON file with the result.\n",
11 | "label": "Letter Count",
12 | "inputs": [
13 | {
14 | "name": "input",
15 | "description": "Input text file"
16 | }
17 | ],
18 | "outputs": [
19 | {
20 | "name": "output",
21 | "description": "Output JSON file"
22 | }
23 | ],
24 | "parameters": [
25 | {
26 | "name": "letter",
27 | "description": "Letter to count",
28 | "type": "str"
29 | }
30 | ],
31 | "attributes": [
32 | {
33 | "name": "wip",
34 | "value": true
35 | }
36 | ],
37 | "tags": [
38 | {
39 | "tag": "tutorial"
40 | }
41 | ]
42 | }
43 | ]
44 | }
--------------------------------------------------------------------------------
/iac/aws_batch/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | package-lock.json
3 | .pytest_cache
4 | *.egg-info
5 |
6 | # Byte-compiled / optimized / DLL files
7 | __pycache__/
8 | *.py[cod]
9 | *$py.class
10 |
11 | # Environments
12 | .env
13 | .venv
14 | env/
15 | venv/
16 | ENV/
17 | env.bak/
18 | venv.bak/
19 |
20 | # CDK Context & Staging files
21 | .cdk.staging/
22 | cdk.out/
23 | cdk.context.json
--------------------------------------------------------------------------------
/iac/aws_batch/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Welcome to your CDK Python project!
3 |
4 | You should explore the contents of this project. It demonstrates a CDK app with an instance of a stack (`aws_batch_stack`)
5 | which contains an Amazon SQS queue that is subscribed to an Amazon SNS topic.
6 |
7 | The `cdk.json` file tells the CDK Toolkit how to execute your app.
8 |
9 | This project is set up like a standard Python project. The initialization process also creates
10 | a virtualenv within this project, stored under the .venv directory. To create the virtualenv
11 | it assumes that there is a `python3` executable in your path with access to the `venv` package.
12 | If for any reason the automatic creation of the virtualenv fails, you can create the virtualenv
13 | manually once the init process completes.
14 |
15 | To manually create a virtualenv on MacOS and Linux:
16 |
17 | ```
18 | $ python3 -m venv .venv
19 | ```
20 |
21 | After the init process completes and the virtualenv is created, you can use the following
22 | step to activate your virtualenv.
23 |
24 | ```
25 | $ source .venv/bin/activate
26 | ```
27 |
28 | If you are a Windows platform, you would activate the virtualenv like this:
29 |
30 | ```
31 | % .venv\Scripts\activate.bat
32 | ```
33 |
34 | Once the virtualenv is activated, you can install the required dependencies.
35 |
36 | ```
37 | $ pip install -r requirements.txt
38 | ```
39 |
40 | At this point you can now synthesize the CloudFormation template for this code.
41 |
42 | ```
43 | $ cdk synth
44 | ```
45 |
46 | You can now begin exploring the source code, contained in the hello directory.
47 | There is also a very trivial test included that can be run like this:
48 |
49 | ```
50 | $ pytest
51 | ```
52 |
53 | To add additional dependencies, for example other CDK libraries, just add to
54 | your requirements.txt file and rerun the `pip install -r requirements.txt`
55 | command.
56 |
57 | ## Useful commands
58 |
59 | * `cdk ls` list all stacks in the app
60 | * `cdk synth` emits the synthesized CloudFormation template
61 | * `cdk deploy` deploy this stack to your default AWS account/region
62 | * `cdk diff` compare deployed stack with current state
63 | * `cdk docs` open CDK documentation
64 |
65 | Enjoy!
66 |
--------------------------------------------------------------------------------
/iac/aws_batch/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/iac/aws_batch/__init__.py
--------------------------------------------------------------------------------
/iac/aws_batch/app.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import aws_cdk as cdk
4 |
5 | from aws_batch.aws_batch_stack import AwsBatchStack
6 |
7 |
8 | app = cdk.App()
9 |
10 | aws_batch_stack = AwsBatchStack(
11 | scope=app,
12 | create_efs=False,
13 | )
14 |
15 | app.synth()
16 |
--------------------------------------------------------------------------------
/iac/aws_batch/aws_batch/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/iac/aws_batch/aws_batch/__init__.py
--------------------------------------------------------------------------------
/iac/aws_batch/aws_batch/stack_config.py:
--------------------------------------------------------------------------------
1 | stack_id = "DendroBatchStack"
2 |
3 | batch_service_role_id = f"{stack_id}-BatchServiceRole"
4 | ecs_instance_role_id = f"{stack_id}-EcsInstanceRole"
5 | batch_jobs_access_role_id = f"{stack_id}-BatchJobsAccessRole"
6 | # efs_file_system_id = f"{stack_id}-EfsFileSystem"
7 | vpc_id = f"{stack_id}-Vpc"
8 | security_group_id = f"{stack_id}-SecurityGroup"
9 | launch_template_id = f"{stack_id}-LaunchTemplate"
10 | compute_env_gpu_id = f"{stack_id}-compute-env-gpu"
11 | compute_env_cpu_id = f"{stack_id}-compute-env-cpu"
12 | job_queue_gpu_id = f"{stack_id}-job-queue-gpu"
13 | job_queue_cpu_id = f"{stack_id}-job-queue-cpu"
14 |
--------------------------------------------------------------------------------
/iac/aws_batch/cdk.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": "python3 app.py",
3 | "watch": {
4 | "include": [
5 | "**"
6 | ],
7 | "exclude": [
8 | "README.md",
9 | "cdk*.json",
10 | "requirements*.txt",
11 | "source.bat",
12 | "**/__init__.py",
13 | "python/__pycache__",
14 | "tests"
15 | ]
16 | },
17 | "context": {
18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
19 | "@aws-cdk/core:checkSecretUsage": true,
20 | "@aws-cdk/core:target-partitions": [
21 | "aws",
22 | "aws-cn"
23 | ],
24 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
25 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
26 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
27 | "@aws-cdk/aws-iam:minimizePolicies": true,
28 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
29 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
30 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
31 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
32 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
33 | "@aws-cdk/core:enablePartitionLiterals": true,
34 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
35 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true,
36 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
37 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
38 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
39 | "@aws-cdk/aws-route53-patters:useCertificate": true,
40 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false,
41 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
42 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
43 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
44 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
45 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
46 | "@aws-cdk/aws-redshift:columnId": true,
47 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
48 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
49 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
50 | "@aws-cdk/aws-kms:aliasNameRef": true,
51 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
52 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
53 | "@aws-cdk/aws-efs:denyAnonymousAccess": true,
54 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
55 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
56 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/iac/aws_batch/devel/create_ami_map.py:
--------------------------------------------------------------------------------
1 | import json
2 | import subprocess
3 |
4 |
5 | def main():
6 | regions = [
7 | 'af-south-1',
8 | 'ap-south-1', 'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1', 'ap-northeast-2', 'ap-south-1', 'ap-east-1',
9 | 'ca-central-1',
10 | 'eu-west-1', 'eu-west-2', 'eu-west-3', 'eu-north-1', 'eu-south-1', 'eu-central-1',
11 | 'me-south-1',
12 | 'sa-east-1',
13 | 'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2'
14 | ]
15 | ami_map = {}
16 | for region in regions:
17 | print(f'Region: {region}')
18 | cmd = f'aws ssm get-parameter --name /aws/service/ecs/optimized-ami/amazon-linux-2/gpu/recommended --region {region} --output json'
19 | # cmd = f'aws ssm get-parameter --name /aws/service/ecs/optimized-ami/amazon-linux-2023/recommended --region {region} --output json'
20 | print(cmd)
21 | try:
22 | output = subprocess.check_output(cmd, shell=True)
23 | output = json.loads(output)
24 | yy = json.loads(output['Parameter']['Value'])
25 | image_id = yy['image_id']
26 | print(f'Image ID: {image_id}')
27 | ami_map[region] = image_id
28 | except Exception as e:
29 | print(f'Error: {e}')
30 | print('')
31 | print('')
32 | continue
33 | print('')
34 | print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
35 | print(json.dumps(ami_map, indent=4))
36 |
37 | if __name__ == '__main__':
38 | main()
39 |
--------------------------------------------------------------------------------
/iac/aws_batch/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | pytest==6.2.5
2 |
--------------------------------------------------------------------------------
/iac/aws_batch/requirements.txt:
--------------------------------------------------------------------------------
1 | aws-cdk-lib==2.106.1
2 | constructs>=10.0.0,<11.0.0
3 |
--------------------------------------------------------------------------------
/iac/aws_batch/source.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | rem The sole purpose of this script is to make the command
4 | rem
5 | rem source .venv/bin/activate
6 | rem
7 | rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows.
8 | rem On Windows, this command just runs this batch file (the argument is ignored).
9 | rem
10 | rem Now we don't need to document a Windows command for activating a virtualenv.
11 |
12 | echo Executing .venv\Scripts\activate.bat for you
13 | .venv\Scripts\activate.bat
14 |
--------------------------------------------------------------------------------
/iac/aws_batch/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/iac/aws_batch/tests/__init__.py
--------------------------------------------------------------------------------
/iac/aws_batch/tests/unit/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/iac/aws_batch/tests/unit/__init__.py
--------------------------------------------------------------------------------
/iac/aws_batch/tests/unit/test_aws_batch_stack.py:
--------------------------------------------------------------------------------
1 | # import aws_cdk as core
2 | # import aws_cdk.assertions as assertions
3 | # from aws_batch.aws_batch_stack import AwsBatchStack
4 |
5 |
6 | # def test_sqs_queue_created():
7 | # app = core.App()
8 | # stack = AwsBatchStack(app, "aws-batch")
9 | # template = assertions.Template.from_stack(stack)
10 |
11 | # template.has_resource_properties("AWS::SQS::Queue", {
12 | # "VisibilityTimeout": 300
13 | # })
14 |
15 |
16 | # def test_sns_topic_created():
17 | # app = core.App()
18 | # stack = AwsBatchStack(app, "aws-batch")
19 | # template = assertions.Template.from_stack(stack)
20 |
21 | # template.resource_count_is("AWS::SNS::Topic", 1)
22 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | dendro
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dendro",
3 | "private": true,
4 | "version": "0.2.15",
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "tsc && vite build",
8 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
9 | "preview": "vite preview",
10 | "deploy": "cp _vercel_prod.json vercel.json && vercel --prod && cp _vercel_dev.json vercel.json"
11 | },
12 | "dependencies": {
13 | "@emotion/react": "^11.10.5",
14 | "@emotion/styled": "^11.10.5",
15 | "@fi-sci/electrode-geometry": "~0.0.1",
16 | "@fi-sci/misc": "~0.0.1",
17 | "@fi-sci/modal-window": "~0.0.1",
18 | "@fi-sci/splitter": "~0.0.1",
19 | "@fortawesome/fontawesome-svg-core": "^6.3.0",
20 | "@fortawesome/free-brands-svg-icons": "^6.3.0",
21 | "@fortawesome/free-solid-svg-icons": "^6.4.0",
22 | "@fortawesome/react-fontawesome": "^0.2.0",
23 | "@isomorphic-git/lightning-fs": "^4.6.0",
24 | "@monaco-editor/react": "^4.6.0",
25 | "@mui/icons-material": "^5.11.16",
26 | "@mui/material": "^5.13.0",
27 | "dompurify": "^3.0.8",
28 | "github-markdown-css": "^5.2.0",
29 | "isomorphic-git": "^1.25.2",
30 | "monaco-editor": "^0.46.0",
31 | "nunjucks": "^3.2.4",
32 | "plotly.js": "^2.27.1",
33 | "pubnub": "^7.4.1",
34 | "react": "^18.2.0",
35 | "react-dom": "^18.2.0",
36 | "react-draggable": "^4.4.5",
37 | "react-ipynb-renderer": "^2.1.4",
38 | "react-markdown": "^8.0.7",
39 | "react-plotly.js": "^2.6.0",
40 | "react-router-dom": "^6.11.1",
41 | "react-syntax-highlighter": "^15.5.0",
42 | "rehype-mathjax": "^4.0.2",
43 | "rehype-raw": "^6.1.1",
44 | "remark-gfm": "^3.0.1",
45 | "remark-math": "^5.1.1"
46 | },
47 | "devDependencies": {
48 | "@types/dompurify": "^3.0.5",
49 | "@types/node": "^20.6.1",
50 | "@types/nunjucks": "^3.2.5",
51 | "@types/pubnub": "^7.3.4",
52 | "@types/react": "^18.0.28",
53 | "@types/react-dom": "^18.0.11",
54 | "@types/react-plotly.js": "^2.6.3",
55 | "@types/react-syntax-highlighter": "^15.5.9",
56 | "@typescript-eslint/eslint-plugin": "^5.57.1",
57 | "@typescript-eslint/parser": "^5.57.1",
58 | "@vitejs/plugin-react": "^4.0.0",
59 | "eslint": "^8.38.0",
60 | "eslint-plugin-react": "^7.32.2",
61 | "eslint-plugin-react-hooks": "^4.6.0",
62 | "eslint-plugin-react-refresh": "^0.3.4",
63 | "typescript": "^5.0.2",
64 | "vite": "^4.3.2"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/public/dendro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/public/dendro.png
--------------------------------------------------------------------------------
/public/help/common.md:
--------------------------------------------------------------------------------
1 | Use the buttons in the top menu bar to configure compute resources, manage projects, set secret keys, or sign in.
2 |
3 | [Dendro documentation](https://flatironinstitute.github.io/dendro-docs/docs/intro)
--------------------------------------------------------------------------------
/public/help/dandiset.md:
--------------------------------------------------------------------------------
1 | # Dandiset
2 |
3 | You are viewing Dandiset {{ route.dandisetId }}.
4 |
5 | To import files from this dandiset, you will need to create an associated project, or open an existing one.
6 |
7 | {% if signedIn %}
8 | {% else %}
9 | **To create a new project, you'll need to sign in.**
10 | {% endif %}
11 |
12 |
13 | {% if staging %}
14 | You are viewing the staging site.
15 | {% endif %}
16 |
--------------------------------------------------------------------------------
/public/help/dandisets.md:
--------------------------------------------------------------------------------
1 | # Browse dandisets
2 |
3 | Search for and select a dandiset.
4 |
5 |
6 | {% if staging %}
7 | You are viewing the staging site. If you need to access embargoed datasets, configure your API key first.
8 |
9 | See [Dandi Archive (staging)](https://gui-staging.dandiarchive.org/).
10 | {% else %}
11 |
12 | See [Dandi Archive](https://dandiarchive.org/).
13 |
14 | {% endif %}
15 |
16 |
--------------------------------------------------------------------------------
/public/help/home.md:
--------------------------------------------------------------------------------
1 | # Dendro home
2 |
3 | Dendro is a web application and compute framework aimed at researchers who want to manage and analyze neurophysiology data.
--------------------------------------------------------------------------------
/public/help/project-dandi-import.md:
--------------------------------------------------------------------------------
1 | # Import files from DANDI
2 |
3 | To import files to this project, use the checkboxes to select the files and then click "Import selected assets". The import may take some time depending on the number of files selected. No data will actually be transferred as the Dendro files are only pointers to the resources. After the files have been imported, they will be visible in the Files tab.
--------------------------------------------------------------------------------
/public/help/project-processors.md:
--------------------------------------------------------------------------------
1 | # Project processors
2 |
3 | You are viewing the processors for this project. These are determined based on the compute resource you have configured for the project. To change the compute resource, go to the project home tab and click the Settings button.
--------------------------------------------------------------------------------
/public/help/project-project-files.md:
--------------------------------------------------------------------------------
1 | # Project files
2 |
3 | You are viewing the files for this project.
--------------------------------------------------------------------------------
/public/help/project-project-home.md:
--------------------------------------------------------------------------------
1 | # Project home
2 |
3 | You are viewing a project. Use the tabs on the left to navigate to the project files and jobs or to import files from DANDI.
--------------------------------------------------------------------------------
/public/help/project-project-jobs.md:
--------------------------------------------------------------------------------
1 | # Project jobs
2 |
3 | You are viewing the jobs for this project.
--------------------------------------------------------------------------------
/public/scripts/load_electrical_series.py:
--------------------------------------------------------------------------------
1 | import h5py
2 | import dendro.client as prc
3 | from dendro.client._interim import NwbRecording
4 | import remfile
5 |
6 |
7 | # Load project {{ project.projectName }}
8 | project = prc.load_project('{{ project.projectId }}')
9 |
10 | # Lazy load {{ fileName }}
11 | nwb_file = remfile.File(project.get_file('{{ fileName }}'))
12 | h5_file = h5py.File(nwb_file, 'r')
13 |
14 | # Create a recording object
15 | recording = NwbRecording(h5_file, electrical_series_path='{{ electricalSeriesPath }}')
16 |
17 | # Get recording information
18 | duration_sec = recording.get_duration()
19 | sampling_frequency = recording.get_sampling_frequency()
20 | num_channels = recording.get_num_channels()
21 |
22 | print(f'Duration (sec): {duration_sec}')
23 | print(f'Sampling frequency (Hz): {sampling_frequency}')
24 | print(f'Number of channels: {num_channels}')
25 |
26 | # Load the first 1000 frames of the recording
27 | traces = recording.get_traces(start_frame=0, end_frame=1000)
28 |
29 | print(f'Traces shape for first 1000 frames: {traces.shape}')
30 |
--------------------------------------------------------------------------------
/public/scripts/load_spike_sorting.py:
--------------------------------------------------------------------------------
1 | import dendro.client as prc
2 | from dendro.client._interim import NwbSorting
3 | import remfile
4 |
5 |
6 | # Load project {{ project.projectName }}
7 | project = prc.load_project('{{ project.projectId }}')
8 |
9 | # Lazy load {{ fileName }}
10 | nwb_file = remfile.File(project.get_file('{{ fileName }}'))
11 |
12 | # Create a recording object
13 | sorting = NwbSorting(nwb_file)
14 |
15 | # Get sorting information
16 | unit_ids = sorting.get_unit_ids()
17 | sampling_frequency = sorting.get_sampling_frequency()
18 |
19 | print(f'Unit ids: {unit_ids}')
20 | print(f'Sampling frequency (Hz): {sampling_frequency}')
21 |
22 | spike_train_1 = sorting.get_unit_spike_train(unit_id=unit_ids[0])
23 | print(f'Number of events for unit {unit_ids[0]}: {len(spike_train_1)}')
24 |
--------------------------------------------------------------------------------
/python/.gitignore:
--------------------------------------------------------------------------------
1 | _dendro
2 | file_cache
3 | script_jobs
4 | example-data
5 | tmp
6 | .dendro-compute-resource-node.yaml
7 |
8 | # Byte-compiled / optimized / DLL files
9 | __pycache__/
10 | *.py[cod]
11 | *$py.class
12 |
13 | # C extensions
14 | *.so
15 |
16 | # Distribution / packaging
17 | .Python
18 | build/
19 | develop-eggs/
20 | dist/
21 | downloads/
22 | eggs/
23 | .eggs/
24 | lib/
25 | lib64/
26 | parts/
27 | sdist/
28 | var/
29 | wheels/
30 | pip-wheel-metadata/
31 | share/python-wheels/
32 | *.egg-info/
33 | .installed.cfg
34 | *.egg
35 | MANIFEST
36 |
37 | # PyInstaller
38 | # Usually these files are written by a python script from a template
39 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
40 | *.manifest
41 | *.spec
42 |
43 | # Installer logs
44 | pip-log.txt
45 | pip-delete-this-directory.txt
46 |
47 | # Unit test / coverage reports
48 | htmlcov/
49 | .tox/
50 | .nox/
51 | .coverage
52 | .coverage.*
53 | .cache
54 | nosetests.xml
55 | coverage.xml
56 | *.cover
57 | *.py,cover
58 | .hypothesis/
59 | .pytest_cache/
60 |
61 | # Translations
62 | *.mo
63 | *.pot
64 |
65 | # Django stuff:
66 | *.log
67 | local_settings.py
68 | db.sqlite3
69 | db.sqlite3-journal
70 |
71 | # Flask stuff:
72 | instance/
73 | .webassets-cache
74 |
75 | # Scrapy stuff:
76 | .scrapy
77 |
78 | # Sphinx documentation
79 | docs/_build/
80 |
81 | # PyBuilder
82 | target/
83 |
84 | # Jupyter Notebook
85 | .ipynb_checkpoints
86 |
87 | # IPython
88 | profile_default/
89 | ipython_config.py
90 |
91 | # pyenv
92 | .python-version
93 |
94 | # pipenv
95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
98 | # install all needed dependencies.
99 | #Pipfile.lock
100 |
101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
102 | __pypackages__/
103 |
104 | # Celery stuff
105 | celerybeat-schedule
106 | celerybeat.pid
107 |
108 | # SageMath parsed files
109 | *.sage.py
110 |
111 | # Environments
112 | .env
113 | .venv
114 | env/
115 | venv/
116 | ENV/
117 | env.bak/
118 | venv.bak/
119 |
120 | # Spyder project settings
121 | .spyderproject
122 | .spyproject
123 |
124 | # Rope project settings
125 | .ropeproject
126 |
127 | # mkdocs documentation
128 | /site
129 |
130 | # mypy
131 | .mypy_cache/
132 | .dmypy.json
133 | dmypy.json
134 |
135 | # Pyre type checker
136 | .pyre/
137 |
--------------------------------------------------------------------------------
/python/README.md:
--------------------------------------------------------------------------------
1 | # dendro
--------------------------------------------------------------------------------
/python/dendro/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from pydantic import VERSION as pydantic_version # noqa
4 |
5 | # In the future we may want to intercept and override the pydantic BaseModel
6 | from pydantic import BaseModel, Field # noqa
7 |
8 | # read the version from thisdir/version.txt
9 | thisdir = os.path.dirname(os.path.realpath(__file__))
10 | with open(os.path.join(thisdir, 'version.txt')) as f:
11 | __version__ = f.read().strip()
12 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/python/dendro/api_helpers/__init__.py
--------------------------------------------------------------------------------
/python/dendro/api_helpers/clients/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/python/dendro/api_helpers/clients/__init__.py
--------------------------------------------------------------------------------
/python/dendro/api_helpers/clients/_get_mongo_client.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from ..core.settings import get_settings
3 | from .MockMongoClient import MockMongoClient
4 | from ...mock import using_mock
5 |
6 | _globals = {"mock_mongo_client": None}
7 |
8 |
9 | # pyright: reportGeneralTypeIssues=false
10 | def _get_mongo_client():
11 | # We want one async mongo client per event loop
12 | loop = asyncio.get_event_loop()
13 | if hasattr(loop, "_mongo_client"):
14 | return loop._mongo_client # type: ignore
15 |
16 | mongo_uri = get_settings().MONGO_URI
17 |
18 | # If we're using a mock client, return it
19 | if using_mock():
20 | client = _globals["mock_mongo_client"] # type: ignore
21 | if client is None:
22 | client = MockMongoClient()
23 | _globals["mock_mongo_client"] = client # type: ignore
24 | else: # pragma: no cover
25 | # Otherwise, create a new client
26 | assert mongo_uri is not None, "MONGO_URI environment variable not set"
27 | from motor.motor_asyncio import AsyncIOMotorClient
28 |
29 | client = AsyncIOMotorClient(mongo_uri)
30 |
31 | # Store the client on the event loop
32 | setattr(loop, "_mongo_client", client)
33 |
34 | return client
35 |
36 |
37 | def _clear_mock_mongo_databases():
38 | client: MockMongoClient = _globals["mock_mongo_client"] # type: ignore
39 | if client is not None:
40 | client.clear_databases()
41 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/clients/_remove_id_field.py:
--------------------------------------------------------------------------------
1 | def _remove_id_field(obj):
2 | if obj is None:
3 | return
4 | if '_id' in obj:
5 | del obj['_id']
6 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/clients/pubsub.py:
--------------------------------------------------------------------------------
1 | import json
2 | import aiohttp
3 | import urllib.parse
4 | from ..core.settings import get_settings
5 | from ...mock import using_mock
6 |
7 |
8 | class PubsubError(Exception):
9 | pass
10 |
11 | async def publish_pubsub_message(*, channel: str, message: dict):
12 | settings = get_settings()
13 | # see https://www.pubnub.com/docs/sdks/rest-api/publish-message-to-channel
14 | sub_key = settings.PUBNUB_SUBSCRIBE_KEY
15 | pub_key = settings.PUBNUB_PUBLISH_KEY
16 | uuid = 'dendro'
17 | # payload is url encoded json
18 | payload = json.dumps(message)
19 | payload = urllib.parse.quote(payload)
20 | url = f"https://ps.pndsn.com/publish/{pub_key}/{sub_key}/0/{channel}/0/{payload}?uuid={uuid}"
21 |
22 | headers = {
23 | 'Accept': 'application/json'
24 | }
25 |
26 | if using_mock():
27 | # don't actually publish the message for the mock case
28 | return True
29 |
30 | # async http get request
31 | async with aiohttp.ClientSession() as session: # pragma: no cover
32 | async with session.get(url, headers=headers) as resp:
33 | if resp.status != 200:
34 | raise PubsubError(f"Error publishing to pubsub: {resp.status} {resp.text}")
35 | return True
36 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/python/dendro/api_helpers/core/__init__.py
--------------------------------------------------------------------------------
/python/dendro/api_helpers/core/_create_random_id.py:
--------------------------------------------------------------------------------
1 | import uuid
2 |
3 |
4 | def _create_random_id(length: int) -> str:
5 | # Generate a random UUID
6 | full_uuid = uuid.uuid4()
7 |
8 | # Convert to a string and take the first [length] characters
9 | short_id = str(full_uuid).replace('-', '')[:length]
10 |
11 | return short_id
12 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/core/_get_project_role.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | from typing import Union
4 | from ...common.dendro_types import DendroProject
5 |
6 |
7 | def _get_project_role(project: DendroProject, user_id: Union[str, None]) -> str:
8 | ADMIN_USER_IDS_JSON = os.getenv('ADMIN_USER_IDS', '[]')
9 | ADMIN_USER_IDS = json.loads(ADMIN_USER_IDS_JSON)
10 | if user_id:
11 | if user_id in ADMIN_USER_IDS:
12 | return 'admin'
13 | if project.ownerId == user_id:
14 | return 'admin'
15 | user = next((x for x in project.users if x.userId == user_id), None)
16 | if user:
17 | return user.role
18 | if project.publiclyReadable:
19 | return 'viewer'
20 | else:
21 | return 'none'
22 |
23 | def _project_has_user(project: DendroProject, user_id: Union[str, None]) -> bool:
24 | if not user_id:
25 | return False
26 | if project.ownerId == user_id:
27 | return True
28 | user = next((x for x in project.users if x.userId == user_id), None)
29 | if user:
30 | return True
31 | return False
32 |
33 | def _check_user_can_read_project(project: DendroProject, user_id: Union[str, None]):
34 | if not _get_project_role(project, user_id) in ['admin', 'editor', 'viewer']:
35 | raise Exception('User does not have read permission for this project')
36 |
37 | def _check_user_can_edit_project(project: DendroProject, user_id: Union[str, None]):
38 | if not _get_project_role(project, user_id) in ['admin', 'editor']:
39 | raise Exception('User does not have edit permission for this project')
40 |
41 | def _check_user_is_project_admin(project: DendroProject, user_id: Union[str, None]):
42 | if not _get_project_role(project, user_id) == 'admin':
43 | raise Exception('User does not have admin permission for this project')
44 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/core/_hide_secret_params_in_job.py:
--------------------------------------------------------------------------------
1 | from ...common.dendro_types import DendroJob
2 |
3 |
4 | def _hide_secret_params_in_job(job: DendroJob):
5 | for param in job.inputParameters:
6 | if param.secret:
7 | param.value = None
8 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/core/_model_dump.py:
--------------------------------------------------------------------------------
1 | def _model_dump(model, exclude_none=False):
2 | # handle both pydantic v1 and v2
3 | if hasattr(model, 'model_dump'):
4 | return model.model_dump(exclude_none=exclude_none)
5 | else:
6 | return model.dict(exclude_none=exclude_none)
7 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/core/settings.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | import os
3 |
4 | # Note: BaseSettings is no longer a part of pydantic package, and I didn't want to add a dependency on pydantic-settings
5 | # So I'm using BaseModel instead
6 |
7 |
8 | class Settings:
9 | def __init__(self) -> None:
10 | # General app config
11 | self.MONGO_URI: Optional[str] = os.environ.get("MONGO_URI")
12 |
13 | self.PUBNUB_SUBSCRIBE_KEY: Optional[str] = os.environ.get("VITE_PUBNUB_SUBSCRIBE_KEY")
14 | self.PUBNUB_PUBLISH_KEY: Optional[str] = os.environ.get("PUBNUB_PUBLISH_KEY")
15 |
16 | self.GITHUB_CLIENT_ID: Optional[str] = os.environ.get("VITE_GITHUB_CLIENT_ID")
17 | self.GITHUB_CLIENT_SECRET: Optional[str] = os.environ.get("GITHUB_CLIENT_SECRET")
18 |
19 | self.DEFAULT_COMPUTE_RESOURCE_ID: Optional[str] = os.environ.get("VITE_DEFAULT_COMPUTE_RESOURCE_ID")
20 |
21 | self.OUTPUT_BUCKET_BASE_URL: Optional[str] = os.environ.get("OUTPUT_BUCKET_BASE_URL")
22 | self.OUTPUT_BUCKET_URI: Optional[str] = os.environ.get("OUTPUT_BUCKET_URI", None)
23 | self.OUTPUT_BUCKET_CREDENTIALS: Optional[str] = os.environ.get("OUTPUT_BUCKET_CREDENTIALS", None)
24 | self.FSBUCKET_SECRET_KEY: Optional[str] = os.environ.get("FSBUCKET_SECRET_KEY", None)
25 |
26 | def get_settings():
27 | return Settings()
28 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/routers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/python/dendro/api_helpers/routers/__init__.py
--------------------------------------------------------------------------------
/python/dendro/api_helpers/routers/client/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/python/dendro/api_helpers/routers/client/__init__.py
--------------------------------------------------------------------------------
/python/dendro/api_helpers/routers/common.py:
--------------------------------------------------------------------------------
1 | import traceback
2 | from fastapi import HTTPException
3 | from functools import wraps
4 |
5 |
6 | class AuthException(Exception):
7 | pass
8 |
9 | def api_route_wrapper(route_func):
10 | @wraps(route_func)
11 | async def wrapper(*args, **kwargs):
12 | try:
13 | return await route_func(*args, **kwargs)
14 | except AuthException as ae:
15 | raise HTTPException(status_code=401, detail=str(ae))
16 | except Exception as e:
17 | traceback.print_exc()
18 | raise HTTPException(status_code=500, detail=str(e)) from e
19 | return wrapper
20 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/routers/compute_resource/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/python/dendro/api_helpers/routers/compute_resource/__init__.py
--------------------------------------------------------------------------------
/python/dendro/api_helpers/routers/gui/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/python/dendro/api_helpers/routers/gui/__init__.py
--------------------------------------------------------------------------------
/python/dendro/api_helpers/routers/gui/create_job_route.py:
--------------------------------------------------------------------------------
1 | from typing import Union
2 | from fastapi import APIRouter, Header
3 |
4 | from ._authenticate_gui_request import _authenticate_gui_request
5 | from ...services.gui.create_job import create_job
6 | from ..common import api_route_wrapper
7 |
8 | from ....common.dendro_types import CreateJobRequest, CreateJobResponse
9 |
10 |
11 | router = APIRouter()
12 |
13 | # create job
14 |
15 | @router.post("/jobs")
16 | @api_route_wrapper
17 | async def create_job_handler(
18 | data: CreateJobRequest,
19 | github_access_token: Union[str, None] = Header(None),
20 | dendro_api_key: Union[str, None] = Header(None),
21 | force_require_approval: bool = False
22 | ) -> CreateJobResponse:
23 | # authenticate the request
24 | user_id = await _authenticate_gui_request(
25 | github_access_token=github_access_token,
26 | dendro_api_key=dendro_api_key,
27 | raise_on_not_authenticated=True
28 | )
29 | assert user_id
30 |
31 | # parse the request
32 | project_id = data.projectId
33 | processor_name = data.processorName
34 | input_files_from_request = data.inputFiles
35 | output_files_from_request = data.outputFiles
36 | input_parameters = data.inputParameters
37 | processor_spec = data.processorSpec
38 | batch_id = data.batchId
39 | dandi_api_key = data.dandiApiKey
40 | required_resources = data.requiredResources
41 | run_method = data.runMethod
42 |
43 | job_id = await create_job(
44 | project_id=project_id,
45 | processor_name=processor_name,
46 | input_files_from_request=input_files_from_request,
47 | output_files_from_request=output_files_from_request,
48 | input_parameters=input_parameters,
49 | processor_spec=processor_spec,
50 | batch_id=batch_id,
51 | user_id=user_id,
52 | dandi_api_key=dandi_api_key,
53 | required_resources=required_resources,
54 | run_method=run_method,
55 | pending_approval=True if force_require_approval else None # None means determine automatically
56 | )
57 |
58 | return CreateJobResponse(
59 | jobId=job_id,
60 | success=True
61 | )
62 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/routers/gui/find_routes.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | from fastapi import APIRouter
3 | from ..common import api_route_wrapper
4 | from .... import BaseModel
5 | from ....common.dendro_types import DendroProject, DendroFile
6 | from ...clients.db import fetch_files_with_content_string, fetch_project, fetch_files_with_metadata
7 |
8 |
9 | router = APIRouter()
10 |
11 | # find projects
12 | class FindProjectsRequest(BaseModel):
13 | fileUrl: str
14 |
15 | class CreateProjectResponse(BaseModel):
16 | projects: List[DendroProject]
17 | success: bool
18 |
19 | @router.post("/find_projects")
20 | @api_route_wrapper
21 | async def find_projects(data: FindProjectsRequest) -> CreateProjectResponse:
22 | files = await fetch_files_with_content_string(f'url:{data.fileUrl}')
23 | project_ids: List[str] = []
24 | for file in files:
25 | if file.projectId not in project_ids:
26 | project_ids.append(file.projectId)
27 | projects: List[DendroProject] = []
28 | for project_id in project_ids:
29 | p = await fetch_project(project_id)
30 | if p is not None:
31 | projects.append(p)
32 | return CreateProjectResponse(projects=projects, success=True)
33 |
34 | # find files with metadata
35 | class FindFilesWithMetadataRequest(BaseModel):
36 | query: dict
37 |
38 | class FindFilesWithMetadataResponse(BaseModel):
39 | files: List[DendroFile]
40 |
41 | @router.post("/find_files_with_metadata")
42 | @api_route_wrapper
43 | async def find_files_with_metadata(data: FindFilesWithMetadataRequest) -> FindFilesWithMetadataResponse:
44 | files = await fetch_files_with_metadata(data.query)
45 | return FindFilesWithMetadataResponse(files=files)
46 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/routers/gui/github_auth_routes.py:
--------------------------------------------------------------------------------
1 | from .... import BaseModel
2 | from fastapi import APIRouter
3 | import aiohttp
4 | from ...core.settings import get_settings
5 |
6 |
7 | router = APIRouter()
8 |
9 | class GithubAuthError(Exception):
10 | pass
11 |
12 | # github auth
13 | class GithubAuthResponse(BaseModel):
14 | access_token: str
15 |
16 | @router.get("/github_auth/{code}")
17 | async def github_auth(code) -> GithubAuthResponse:
18 | settings = get_settings()
19 | GITHUB_CLIENT_ID = settings.GITHUB_CLIENT_ID
20 | GITHUB_CLIENT_SECRET = settings.GITHUB_CLIENT_SECRET
21 | assert GITHUB_CLIENT_ID, 'Env var not set: VITE_GITHUB_CLIENT_ID'
22 | assert GITHUB_CLIENT_SECRET, 'Env var not set: GITHUB_CLIENT_SECRET'
23 | url = f'https://github.com/login/oauth/access_token?client_id={GITHUB_CLIENT_ID}&client_secret={GITHUB_CLIENT_SECRET}&code={code}'
24 | headers = {
25 | 'accept': 'application/json'
26 | }
27 | async with aiohttp.ClientSession() as session:
28 | async with session.get(url, headers=headers) as resp:
29 | r = await resp.json()
30 | if 'access_token' in r:
31 | return GithubAuthResponse(access_token=r['access_token']) # pragma: no cover
32 | elif 'error' in r:
33 | raise GithubAuthError(f'Error in github oauth response: {r["error"]}')
34 | else:
35 | raise Exception('No access_token in github oauth response.') # pragma: no cover
36 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/routers/gui/router.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter
2 | from .create_job_route import router as create_job_router
3 | from .project_routes import router as project_router
4 | from .compute_resource_routes import router as compute_resource_router
5 | from .file_routes import router as file_router
6 | from .job_routes import router as job_router
7 | from .script_routes import router as script_router
8 | from .github_auth_routes import router as github_auth_router
9 | from .user_routes import router as user_router
10 | from .usage_routes import router as usage_router
11 | from .find_routes import router as find_router
12 |
13 | router = APIRouter()
14 |
15 | router.include_router(create_job_router)
16 | router.include_router(project_router, prefix="/projects")
17 | router.include_router(compute_resource_router, prefix="/compute_resources")
18 | router.include_router(file_router)
19 | router.include_router(job_router, prefix="/jobs")
20 | router.include_router(script_router, prefix="/scripts")
21 | router.include_router(github_auth_router)
22 | router.include_router(user_router, prefix="/users")
23 | router.include_router(usage_router, prefix="/usage")
24 | router.include_router(find_router)
25 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/routers/gui/usage_routes.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | from fastapi import APIRouter, Header
4 | from .... import BaseModel
5 | from ....common.dendro_types import ComputeResourceUserUsage
6 | from ._authenticate_gui_request import _authenticate_gui_request
7 | from ..common import api_route_wrapper
8 | from ...services.gui.get_compute_resource_user_usage import get_compute_resource_user_usage
9 |
10 |
11 | router = APIRouter()
12 |
13 | # get usage
14 | class GetUsageResponse(BaseModel):
15 | usage: ComputeResourceUserUsage
16 | success: bool
17 |
18 | @router.get("/compute_resource/{compute_resource_id}/user/{user_id}")
19 | @api_route_wrapper
20 | async def get_usage(compute_resource_id, user_id, github_access_token: str = Header(...)):
21 | # url decode the %7C in the user_id
22 | user_id = user_id.replace('%7C', '|')
23 |
24 | # authenticate the request
25 | auth_user_id = await _authenticate_gui_request(github_access_token=github_access_token, raise_on_not_authenticated=True)
26 | assert auth_user_id
27 |
28 | if user_id != auth_user_id:
29 | ADMIN_USER_IDS_JSON = os.getenv('ADMIN_USER_IDS', '[]')
30 | ADMIN_USER_IDS = json.loads(ADMIN_USER_IDS_JSON)
31 |
32 | if user_id not in ADMIN_USER_IDS:
33 | raise Exception(f'User is not admin and cannot view usage for other users ({user_id} != {auth_user_id})')
34 |
35 | # get usage
36 | usage = await get_compute_resource_user_usage(compute_resource_id=compute_resource_id, user_id=user_id)
37 |
38 | return GetUsageResponse(usage=usage, success=True)
39 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/routers/gui/user_routes.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import uuid
3 | from fastapi import APIRouter, Header
4 | from pydantic import BaseModel
5 | from ..common import api_route_wrapper
6 | from ._authenticate_gui_request import _authenticate_gui_request
7 | from ...clients.db import set_dendro_api_key_for_user
8 |
9 |
10 | router = APIRouter()
11 |
12 | class CreateDendroApiKeyResponse(BaseModel):
13 | dendroApiKey: str
14 | success: bool
15 |
16 | @router.post("/{user_id}/dendro_api_key")
17 | @api_route_wrapper
18 | async def create_dendro_api_key(user_id, github_access_token: str = Header(...)):
19 | # authenticate the request
20 | user_id = await _authenticate_gui_request(github_access_token=github_access_token, raise_on_not_authenticated=True)
21 | assert user_id
22 |
23 | # create the api key as the sha1 hash a of uuid
24 | new_dendro_api_key = hashlib.sha1(str(uuid.uuid4()).encode('utf-8')).hexdigest()
25 |
26 | # set the api key for the user
27 | await set_dendro_api_key_for_user(user_id, new_dendro_api_key)
28 |
29 | return CreateDendroApiKeyResponse(dendroApiKey=new_dendro_api_key, success=True)
30 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/routers/processor/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/python/dendro/api_helpers/routers/processor/__init__.py
--------------------------------------------------------------------------------
/python/dendro/api_helpers/services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/python/dendro/api_helpers/services/__init__.py
--------------------------------------------------------------------------------
/python/dendro/api_helpers/services/_remove_detached_files_and_jobs.py:
--------------------------------------------------------------------------------
1 | from ..clients._get_mongo_client import _get_mongo_client
2 | from ..clients._remove_id_field import _remove_id_field
3 | from ...common.dendro_types import DendroFile, DendroJob
4 |
5 |
6 | async def _remove_detached_files_and_jobs(project_id: str):
7 | client = _get_mongo_client()
8 | files_collection = client['dendro']['files']
9 | jobs_collection = client['dendro']['jobs']
10 |
11 | files = await files_collection.find({
12 | 'projectId': project_id
13 | }).to_list(length=None) # type: ignore
14 | jobs = await jobs_collection.find({
15 | 'projectId': project_id,
16 | 'deleted': {'$ne': True}
17 | }).to_list(length=None) # type: ignore
18 |
19 | for file in files:
20 | _remove_id_field(file)
21 | for job in jobs:
22 | _remove_id_field(job)
23 | files = [DendroFile(**x) for x in files]
24 | jobs = [DendroJob(**x) for x in jobs]
25 |
26 | something_changed = True
27 | while something_changed:
28 | file_ids = set(x.fileId for x in files)
29 | job_ids = set(x.jobId for x in jobs)
30 |
31 | job_ids_to_delete = set()
32 | for job in jobs:
33 | if any(x not in file_ids for x in job.inputFileIds):
34 | job_ids_to_delete.add(job.jobId)
35 | if job.outputFileIds:
36 | if any(x not in file_ids for x in job.outputFileIds):
37 | job_ids_to_delete.add(job.jobId)
38 | file_ids_to_delete = set()
39 | for file in files:
40 | if file.jobId:
41 | if file.jobId not in job_ids:
42 | file_ids_to_delete.add(file.fileId)
43 | something_changed = False
44 | if len(job_ids_to_delete) > 0:
45 | something_changed = True
46 | # Let's actually delete them rather than just marking them as deleted
47 | jobs_collection.delete_many({
48 | 'jobId': {'$in': list(job_ids_to_delete)}
49 | })
50 | # for job_id in job_ids_to_delete:
51 | # await jobs_collection.update_one({
52 | # 'jobId': job_id
53 | # }, {
54 | # '$set': {
55 | # 'deleted': True
56 | # }
57 | # })
58 | jobs = [x for x in jobs if x.jobId not in job_ids_to_delete]
59 | if len(file_ids_to_delete) > 0:
60 | something_changed = True
61 | await files_collection.delete_many({
62 | 'fileId': {'$in': list(file_ids_to_delete)}
63 | })
64 | files = [x for x in files if x.fileId not in file_ids_to_delete]
65 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/services/gui/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/python/dendro/api_helpers/services/gui/__init__.py
--------------------------------------------------------------------------------
/python/dendro/api_helpers/services/gui/delete_project.py:
--------------------------------------------------------------------------------
1 | from ....common.dendro_types import DendroProject
2 | from ...clients.db import delete_all_files_in_project, delete_all_jobs_in_project, delete_project as db_delete_project
3 |
4 |
5 | async def delete_project(project: DendroProject):
6 | await delete_all_files_in_project(project.projectId)
7 | await delete_all_jobs_in_project(project.projectId)
8 | await db_delete_project(project.projectId)
9 | return None
10 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/services/gui/get_compute_resource_user_usage.py:
--------------------------------------------------------------------------------
1 | from ....common.dendro_types import ComputeResourceUserUsage
2 | from ...clients.db import fetch_jobs_including_deleted
3 |
4 |
5 | async def get_compute_resource_user_usage(*, compute_resource_id: str, user_id: str) -> ComputeResourceUserUsage:
6 | jobs_including_deleted = await fetch_jobs_including_deleted(compute_resource_id=compute_resource_id, user_id=user_id)
7 | return ComputeResourceUserUsage(
8 | computeResourceId=compute_resource_id,
9 | userId=user_id,
10 | jobsIncludingDeleted=jobs_including_deleted
11 | )
12 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/services/gui/set_file.py:
--------------------------------------------------------------------------------
1 | import time
2 | from typing import Union
3 | from ...clients.db import fetch_file, delete_file, insert_file, update_project, update_file_metadata
4 | from ....common.dendro_types import DendroFile
5 | from ...core._create_random_id import _create_random_id
6 | from .._remove_detached_files_and_jobs import _remove_detached_files_and_jobs
7 |
8 |
9 | async def set_file(
10 | user_id: str,
11 | project_id: str,
12 | file_name: str,
13 | content: str, # for example, url:https://...
14 | job_id: Union[str, None],
15 | size: int,
16 | metadata: dict,
17 | is_folder: bool = False
18 | ):
19 | existing_file = await fetch_file(project_id, file_name)
20 | if existing_file is not None:
21 | await delete_file(project_id, file_name)
22 | deleted_old_file = True
23 | else:
24 | deleted_old_file = False
25 |
26 | new_file = DendroFile(
27 | projectId=project_id,
28 | fileId=_create_random_id(8),
29 | userId=user_id,
30 | fileName=file_name,
31 | size=size,
32 | timestampCreated=time.time(),
33 | content=content,
34 | metadata=metadata,
35 | isFolder=is_folder,
36 | jobId=job_id
37 | )
38 | await insert_file(new_file)
39 |
40 | if deleted_old_file:
41 | await _remove_detached_files_and_jobs(project_id)
42 |
43 | await update_project(
44 | project_id=project_id,
45 | update={
46 | 'timestampModified': time.time()
47 | }
48 | )
49 |
50 | return new_file.fileId
51 |
52 | async def set_file_metadata(
53 | project_id: str,
54 | file_name: str,
55 | metadata: dict
56 | ):
57 | existing_file = await fetch_file(project_id, file_name)
58 | if existing_file is None:
59 | raise Exception(f"Cannot set metadata. File {file_name} not found in project {project_id}")
60 | await update_file_metadata(
61 | project_id=project_id,
62 | file_id=existing_file.fileId,
63 | metadata=metadata
64 | )
65 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/services/processor/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/python/dendro/api_helpers/services/processor/__init__.py
--------------------------------------------------------------------------------
/python/dendro/api_helpers/services/processor/_get_fsbucket_signed_upload_url.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | import time
3 | import hashlib
4 |
5 |
6 | def _get_fsbucket_signed_upload_url(
7 | *, fsbucket_api_url, secret_key: str, object_key: str, size: Optional[int] = None
8 | ):
9 | # expires in one hour
10 | expires = int(time.time()) + 3600
11 | signature = _create_signature(
12 | secret_key=secret_key, object_key=object_key, expires=expires, method="PUT"
13 | )
14 | url = f"{fsbucket_api_url}/{object_key}?signature={signature}&expires={expires}"
15 | return url
16 |
17 |
18 | def _create_signature(*, secret_key: str, object_key: str, expires: int, method: str):
19 | path = f'/{object_key}'
20 | string_to_sign = f"{method}\n{path}\n{expires}\n{secret_key}"
21 | hash = hashlib.sha256()
22 | hash.update(string_to_sign.encode("utf-8"))
23 | return hash.hexdigest()
24 |
--------------------------------------------------------------------------------
/python/dendro/api_helpers/services/processor/_get_signed_upload_url.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Dict, Any
2 | import boto3
3 | from boto3.session import Config
4 | import json
5 |
6 |
7 | async def _get_signed_upload_url(*,
8 | bucket_uri: str,
9 | bucket_credentials: str,
10 | object_key: str,
11 | size: Optional[int] = None
12 | ):
13 | creds = json.loads(bucket_credentials)
14 | access_key_id = creds['accessKeyId']
15 | secret_access_key = creds['secretAccessKey']
16 | endpoint = creds.get('endpoint', None)
17 |
18 | region_name = _get_region_name_from_uri(bucket_uri)
19 | bucket_name = _get_bucket_name_from_uri(bucket_uri)
20 |
21 | s3_client = boto3.client(
22 | 's3',
23 | aws_access_key_id=access_key_id,
24 | aws_secret_access_key=secret_access_key,
25 | endpoint_url=endpoint,
26 | region_name=region_name,
27 | config=Config(signature_version='s3v4')
28 | )
29 |
30 | params: Dict[str, Any] = {
31 | 'Bucket': bucket_name,
32 | 'Key': object_key
33 | }
34 | if size is not None:
35 | params['ContentLength'] = size
36 |
37 | return s3_client.generate_presigned_url(
38 | 'put_object',
39 | Params=params,
40 | ExpiresIn=30 * 60 # 30 minutes
41 | )
42 |
43 | def _get_bucket_name_from_uri(bucket_uri: str) -> str:
44 | if not bucket_uri:
45 | return ''
46 | return bucket_uri.split('?')[0].split('/')[2]
47 |
48 | def _get_region_name_from_uri(bucket_uri: str) -> str:
49 | # for example: s3://bucket-name?region=us-west-2
50 | if not bucket_uri:
51 | return ''
52 | default_region = 'auto' # for cloudflare
53 | a = bucket_uri.split('?')
54 | if len(a) == 1:
55 | return default_region
56 | b = a[1].split('&')
57 | c = [x for x in b if x.startswith('region=')]
58 | if len(c) == 0:
59 | return default_region
60 | d = c[0].split('=')
61 | if len(d) != 2:
62 | return default_region
63 | return d[1]
64 |
--------------------------------------------------------------------------------
/python/dendro/aws_batch/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/python/dendro/aws_batch/__init__.py
--------------------------------------------------------------------------------
/python/dendro/client/__init__.py:
--------------------------------------------------------------------------------
1 | from .Project import Project, load_project # noqa: F401
2 | from .submit_job import submit_job, SubmitJobInputFile, SubmitJobOutputFile, SubmitJobParameter # noqa: F401
3 | from .set_file import set_file, set_file_metadata # noqa: F401
4 | from ..common.dendro_types import DendroJobRequiredResources # noqa: F401
5 | from ._upload_blob import upload_bytes_blob, upload_file_blob, upload_json_blob, upload_text_blob # noqa: F401
6 | from ._create_batch_id import create_batch_id # noqa: F401
7 |
--------------------------------------------------------------------------------
/python/dendro/client/_create_batch_id.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 |
4 | def create_batch_id() -> str:
5 | """Create a new batch ID string.
6 |
7 | This is a random string that can be used when submitting multiple jobs.
8 | """
9 | choices = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
10 | return ''.join(random.choices(choices, k=8))
11 |
--------------------------------------------------------------------------------
/python/dendro/client/_interim/NwbSorting.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import h5py
3 |
4 |
5 | def NwbSorting(file):
6 | h5_file = h5py.File(file, 'r')
7 |
8 | # Load unit IDs
9 | ids: np.ndarray = h5_file['units']['id'][:] # type: ignore
10 |
11 | # Load spike times index
12 | spike_times_index: np.ndarray = h5_file['units']['spike_times_index'][:] # type: ignore
13 |
14 | # Load spike times
15 | spike_times: np.ndarray = h5_file['units']['spike_times'][:] # type: ignore
16 |
17 | units_dict = {}
18 | sampling_frequency = 30000 # TODO: get this from the NWB file
19 | for i in range(len(ids)):
20 | if i == 0:
21 | s = spike_times[0:spike_times_index[0]]
22 | else:
23 | s = spike_times[spike_times_index[i - 1]:spike_times_index[i]]
24 | units_dict[ids[i]] = (s * sampling_frequency).astype(np.int32)
25 | sorting = _numpy_sorting_from_dict([units_dict], sampling_frequency=sampling_frequency)
26 | return sorting
27 |
28 | def _numpy_sorting_from_dict(units_dict_list, *, sampling_frequency):
29 | import spikeinterface as si # type: ignore
30 | try:
31 | # different versions of spikeinterface
32 | # see: https://github.com/SpikeInterface/spikeinterface/issues/2083
33 | sorting = si.NumpySorting.from_dict( # type: ignore
34 | units_dict_list, sampling_frequency=sampling_frequency # type: ignore
35 | )
36 | except: # noqa
37 | sorting = si.NumpySorting.from_unit_dict( # type: ignore
38 | units_dict_list, sampling_frequency=sampling_frequency # type: ignore
39 | )
40 | return sorting
41 |
--------------------------------------------------------------------------------
/python/dendro/client/_interim/__init__.py:
--------------------------------------------------------------------------------
1 | from .NwbRecording import NwbRecording # noqa: F401
2 | from .NwbSorting import NwbSorting # noqa: F401
3 |
--------------------------------------------------------------------------------
/python/dendro/common/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/python/dendro/common/__init__.py
--------------------------------------------------------------------------------
/python/dendro/compute_resource/ComputeResourceException.py:
--------------------------------------------------------------------------------
1 | class ComputeResourceException(Exception):
2 | pass
3 |
--------------------------------------------------------------------------------
/python/dendro/compute_resource/PubsubClient.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | import queue
3 | from pubnub.pnconfiguration import PNConfiguration
4 | from pubnub.callbacks import SubscribeCallback
5 | from pubnub.pubnub import PubNub
6 |
7 | class MySubscribeCallback(SubscribeCallback):
8 | def __init__(self, message_queue: queue.Queue, compute_resource_id: str):
9 | self._message_queue = message_queue
10 | self._compute_resource_id = compute_resource_id
11 | def message(self, pubnub, message):
12 | msg = message.message
13 | if msg.get('computeResourceId', None) == self._compute_resource_id:
14 | self._message_queue.put(msg)
15 |
16 | class PubsubClient:
17 | def __init__(self, *,
18 | pubnub_subscribe_key: str,
19 | pubnub_channel: str,
20 | pubnub_user: str,
21 | compute_resource_id: str
22 | ):
23 | self._message_queue = queue.Queue()
24 | pnconfig = PNConfiguration()
25 | pnconfig.subscribe_key = pubnub_subscribe_key # type: ignore (not sure why we need to type ignore this)
26 | pnconfig.user_id = pubnub_user
27 | pnconfig.uuid = compute_resource_id
28 | self._pubnub = PubNub(pnconfig)
29 | self._listener = MySubscribeCallback(message_queue=self._message_queue, compute_resource_id=compute_resource_id)
30 | self._pubnub.add_listener(self._listener)
31 | self._pubnub.subscribe().channels([pubnub_channel]).execute()
32 | def take_messages(self) -> List[dict]:
33 | ret = []
34 | while True:
35 | try:
36 | msg = self._message_queue.get(block=False)
37 | ret.append(msg)
38 | except queue.Empty:
39 | break
40 | return ret
41 | def close(self):
42 | self._pubnub.unsubscribe_all()
43 | self._pubnub.stop()
44 | self._pubnub.remove_listener(self._listener)
45 | # unfortunately this doesn't actually kill the thread
46 | # I submitted a ticket to pubnub about this
47 | # and they acknowledged that it's a problem
48 | # but they don't seem to be fixing it
49 |
--------------------------------------------------------------------------------
/python/dendro/compute_resource/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/python/dendro/compute_resource/__init__.py
--------------------------------------------------------------------------------
/python/dendro/internal_job_monitoring/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flatironinstitute/dendro-old/ad3211be0d92a255360801eef68be4f5aca7201e/python/dendro/internal_job_monitoring/__init__.py
--------------------------------------------------------------------------------
/python/dendro/internal_job_monitoring/common.py:
--------------------------------------------------------------------------------
1 | import os
2 | from ..common._api_request import _processor_get_api_request
3 |
4 |
5 | def _get_upload_url(*, job_id: str, job_private_key: str, output_name: str) -> str:
6 | """Get a signed upload URL for the output (console or resource log) of a job"""
7 | url_path = f'/api/processor/jobs/{job_id}/outputs/{output_name}/upload_url'
8 | headers = {
9 | 'job-private-key': job_private_key
10 | }
11 | res = _processor_get_api_request(
12 | url_path=url_path,
13 | headers=headers
14 | )
15 | return res['uploadUrl']
16 |
17 | def _process_is_alive(pid: str) -> bool:
18 | """
19 | Check if a process is alive.
20 | """
21 | try:
22 | os.kill(int(pid), 0)
23 | return True
24 | except OSError:
25 | return False
26 |
--------------------------------------------------------------------------------
/python/dendro/internal_job_monitoring/job_status_monitor.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 | from .common import _process_is_alive
4 | from ..common._api_request import _processor_get_api_request
5 |
6 |
7 | def job_status_monitor(parent_pid: str):
8 | """
9 | Monitor job status to see if the job was canceled.
10 | """
11 | job_id = os.environ.get('JOB_ID', None)
12 | if job_id is None:
13 | raise KeyError('JOB_ID is not set')
14 | job_private_key = os.environ.get('JOB_PRIVATE_KEY', None)
15 | if job_private_key is None:
16 | raise KeyError('JOB_PRIVATE_KEY is not set')
17 | cancel_out_file = os.environ.get('CANCEL_OUT_FILE', None)
18 | if cancel_out_file is None:
19 | raise KeyError('CANCEL_OUT_FILE is not set')
20 |
21 | last_check_timestamp = 0
22 | overall_timer = time.time()
23 |
24 | while True:
25 | if not _process_is_alive(parent_pid):
26 | print(f'Parent process {parent_pid} is no longer alive. Exiting.')
27 | break
28 |
29 | elapsed_since_check = time.time() - last_check_timestamp
30 | overall_elapsed = time.time() - overall_timer
31 | if overall_elapsed < 60:
32 | interval = 10
33 | elif overall_elapsed < 60 * 5:
34 | interval = 30
35 | elif overall_elapsed < 60 * 20:
36 | interval = 60
37 | else:
38 | interval = 120
39 | if elapsed_since_check >= interval:
40 | last_check_timestamp = time.time()
41 | try:
42 | status = _get_job_status(job_id=job_id, job_private_key=job_private_key)
43 | if status != 'running':
44 | print(f'Job status is {status}. Canceling.')
45 | with open(cancel_out_file, 'w') as f:
46 | if isinstance(status, str):
47 | f.write(status)
48 | else:
49 | f.write('0')
50 | break
51 | else:
52 | print('Job status is running')
53 | except: # noqa
54 | # maybe there was a network error
55 | print('Error getting job status')
56 |
57 | time.sleep(1)
58 |
59 | def _get_job_status(*, job_id: str, job_private_key: str) -> str:
60 | """Get a job status from the dendro API"""
61 | url_path = f'/api/processor/jobs/{job_id}/status'
62 | headers = {
63 | 'job-private-key': job_private_key
64 | }
65 | res = _processor_get_api_request(
66 | url_path=url_path,
67 | headers=headers
68 | )
69 | return res['status']
70 |
--------------------------------------------------------------------------------
/python/dendro/mock.py:
--------------------------------------------------------------------------------
1 | _globals = {
2 | 'use_mock': False
3 | }
4 |
5 | def using_mock() -> bool:
6 | return _globals['use_mock']
7 |
8 | def set_use_mock(use_mock: bool):
9 | _globals['use_mock'] = use_mock
10 |
--------------------------------------------------------------------------------
/python/dendro/sdk/FileManifest.py:
--------------------------------------------------------------------------------
1 | from typing import List, Union
2 | from pydantic import BaseModel
3 |
4 |
5 | class FileManifestFile(BaseModel):
6 | name: str
7 | url: str
8 | size: Union[int, None] = None
9 |
10 | class FileManifest(BaseModel):
11 | files: List[FileManifestFile]
12 |
--------------------------------------------------------------------------------
/python/dendro/sdk/ProcessorBase.py:
--------------------------------------------------------------------------------
1 | from typing import Any, List
2 |
3 | class ProcessorBase:
4 | name: str
5 | label: str
6 | description: str
7 | tags: List[str]
8 | attributes: dict
9 |
10 | @staticmethod
11 | def run(
12 | context: Any
13 | ):
14 | raise NotImplementedError()
15 |
--------------------------------------------------------------------------------
/python/dendro/sdk/__init__.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa
2 |
3 | from .InputFile import InputFile
4 | from .InputFolder import InputFolder
5 | from .OutputFile import OutputFile
6 | from .OutputFolder import OutputFolder
7 | from .App import App
8 |
9 | from .get_project_file_from_uri import get_project_file_from_uri
10 |
11 | from .ProcessorBase import ProcessorBase
12 |
13 | from .. import BaseModel, Field
14 |
--------------------------------------------------------------------------------
/python/dendro/sdk/_load_spec_from_uri.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import os
3 | import json
4 | import tempfile
5 |
6 | def _load_spec_from_uri(uri: str) -> dict:
7 | # Convert github blob URL to raw URL
8 | if (uri.startswith('https://github.com/')) and ('/blob/' in uri):
9 | raw_url = uri.replace('github.com', 'raw.githubusercontent.com').replace('/blob/', '/') + f'?cachebust={os.urandom(16).hex()}'
10 | print(f'URL: {raw_url}')
11 | else:
12 | raw_url = uri
13 |
14 | if raw_url.startswith('file://'):
15 | # Read the content from a local file
16 | with open(raw_url[len('file://'):], 'r', encoding='utf-8') as file:
17 | content = file.read()
18 | else:
19 | # Download the content
20 | response = requests.get(raw_url, timeout=60)
21 | response.raise_for_status()
22 | content = response.text
23 |
24 | # Save to a temporary file
25 | with tempfile.NamedTemporaryFile(delete=False, mode='w', suffix='.json') as temp_file:
26 | temp_file.write(content)
27 | temp_file_path = temp_file.name
28 |
29 | # Read the JSON content from the file
30 | with open(temp_file_path, 'r', encoding='utf-8') as file:
31 | data = json.load(file)
32 |
33 | # Clean up the temporary file
34 | os.remove(temp_file_path)
35 |
36 | return data
37 |
--------------------------------------------------------------------------------
/python/dendro/sdk/_make_spec_file.py:
--------------------------------------------------------------------------------
1 | import os
2 | import subprocess
3 | from pathlib import Path
4 |
5 |
6 | def make_app_spec_file_function(app_dir: str, spec_output_file: str):
7 | # Ensure the directory path is an absolute path
8 | app_dir_path = Path(app_dir).resolve()
9 |
10 | if spec_output_file is None:
11 | spec_output_file = str(app_dir_path / 'spec.json')
12 |
13 | script_path = str(app_dir_path / 'main.py')
14 |
15 | # check whether script path is executable
16 | if not os.access(script_path, os.X_OK):
17 | raise Exception(f"Script {script_path} is not executable")
18 |
19 | env = os.environ.copy()
20 | env['SPEC_OUTPUT_FILE'] = spec_output_file
21 | subprocess.run([script_path], env=env)
22 |
23 | # When we do it the following way, the inspection of type hints in the processor context does not work
24 |
25 | # # Construct the absolute path to main.py in the specified directory
26 | # main_module_path = app_dir_path / 'main.py'
27 |
28 | # # Check if main.py exists
29 | # if not main_module_path.exists():
30 | # raise FileNotFoundError(f"main.py not found in {app_dir_path}")
31 |
32 | # # Create a module name from the directory path
33 | # module_name = app_dir_path.name
34 |
35 | # # Use importlib to load the module
36 | # spec = importlib.util.spec_from_file_location(module_name, str(main_module_path))
37 | # if spec is None:
38 | # raise ImportError(f"Unable to get spec for module {module_name} from {main_module_path}")
39 | # module = importlib.util.module_from_spec(spec)
40 | # if spec.loader is None:
41 | # raise ImportError(f"Unable to get loader for module {module_name} from {main_module_path}")
42 | # spec.loader.exec_module(module)
43 |
44 | # # Check if the App class exists in the loaded module
45 | # if hasattr(module, 'app') and isinstance(getattr(module, 'app'), pr.App):
46 | # # Create an instance of the App class
47 | # app_instance = module.app
48 |
49 | # # Call the make_spec_file method
50 | # app_instance.make_spec_file(spec_output_file)
51 | # else:
52 | # raise AttributeError("App class not found in main.py")
53 |
--------------------------------------------------------------------------------
/python/dendro/sdk/_test_app_processor.py:
--------------------------------------------------------------------------------
1 | import os
2 | import subprocess
3 | from pathlib import Path
4 |
5 |
6 | def test_app_processor_function(app_dir: str, processor: str, context: str):
7 | # Ensure the directory path is an absolute path
8 | app_dir_path = Path(app_dir).resolve()
9 |
10 | executable_path = str(app_dir_path / 'main.py')
11 |
12 | env = os.environ.copy()
13 | env['TEST_APP_PROCESSOR'] = '1'
14 | env['PROCESSOR_NAME'] = processor
15 | env['CONTEXT_FILE'] = context
16 | subprocess.run([executable_path], env=env)
17 |
--------------------------------------------------------------------------------
/python/dendro/sdk/get_project_file_from_uri.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | def get_project_file_from_uri(uri: str):
5 | from .Job import _parse_dendro_uri
6 | from .InputFile import InputFile
7 |
8 | job_id = os.environ.get('JOB_ID')
9 | if not job_id:
10 | raise Exception('Cannot get project file when JOB_ID is not set')
11 | job_private_key = os.environ.get('JOB_PRIVATE_KEY')
12 | if not job_private_key:
13 | raise Exception('Cannot get project file when JOB_PRIVATE_KEY is not set')
14 |
15 | file_id, is_folder, label = _parse_dendro_uri(uri)
16 | if is_folder:
17 | raise Exception('Cannot get project file for a folder')
18 | ret = InputFile(
19 | name=None,
20 | url=None,
21 | local_file_name=None,
22 | project_file_uri=uri, # use this instead of name
23 | project_file_name=label,
24 | job_id=job_id,
25 | job_private_key=job_private_key
26 | )
27 | ret._check_file_cache()
28 | return ret
29 |
--------------------------------------------------------------------------------
/python/dendro/version.txt:
--------------------------------------------------------------------------------
1 | 0.2.17
--------------------------------------------------------------------------------
/python/mock-output-file.txt:
--------------------------------------------------------------------------------
1 | mock output
--------------------------------------------------------------------------------
/python/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | markers =
3 | api: tests for api that require the api packages to be installed
--------------------------------------------------------------------------------
/python/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | # read version from dendro/version.txt
4 | with open('dendro/version.txt') as f:
5 | __version__ = f.read().strip()
6 |
7 | setup(
8 | name='dendro',
9 | version=__version__,
10 | author="Jeremy Magland, Luiz Tauffer",
11 | author_email="jmagland@flatironinstitute.org",
12 | url="https://github.com/flatironinstitute/dendro",
13 | description="Web framework for neurophysiology data analysis",
14 | packages=find_packages(),
15 | include_package_data=True,
16 | package_data={'dendro': ['version.txt']},
17 | install_requires=[
18 | 'click',
19 | 'simplejson',
20 | 'numpy',
21 | 'PyYAML',
22 | 'remfile',
23 | 'pydantic', # intentionally do not specify version 1 or 2 since we support both
24 | 'cryptography',
25 | 'h5py>=3.10.0',
26 | 'psutil'
27 | ],
28 | extras_require={
29 | 'compute_resource': [
30 | 'pubnub>=7.2.0',
31 | 'boto3'
32 | ],
33 | 'api': [
34 | 'fastapi',
35 | 'motor',
36 | 'simplejson',
37 | 'pydantic',
38 | 'aiohttp',
39 | 'boto3'
40 | ]
41 | },
42 | entry_points={
43 | "console_scripts": [
44 | "dendro=dendro.cli:main",
45 | ],
46 | }
47 | )
48 |
--------------------------------------------------------------------------------
/python/tests/mock_app/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from typing import List, Optional
4 | import time
5 | import os
6 | from dendro import BaseModel, Field
7 | from dendro.sdk import App, ProcessorBase, InputFile, OutputFile
8 |
9 |
10 | class MockParameterGroup(BaseModel):
11 | num: int = Field(description='Number', default=1)
12 | secret_param: str = Field(description='Secret param', default='123', json_schema_extra={'secret': True})
13 |
14 | class MockProcessor1Context(BaseModel):
15 | input_file: InputFile = Field(description='Input file')
16 | input_list: List[InputFile] = Field(description='Input file list')
17 | output_file: OutputFile = Field(description='Output file')
18 | text1: str = Field(description='Text 1', default='abc')
19 | text2: str = Field(description='Text 2')
20 | text3: str = Field(description='Text 3', default='xyz', json_schema_extra={'options': ['abc', 'xyz']})
21 | val1: float = Field(description='Value 1', default=1.0)
22 | val2: Optional[float] = Field(description='Value 2 could be a float or it could be None')
23 | group: MockParameterGroup = Field(description='Group', default=MockParameterGroup())
24 | intentional_error: bool = Field(description='Intentional error', default=False)
25 |
26 | class MockProcessor1(ProcessorBase):
27 | name = 'mock-processor1'
28 | description = 'This is mock processor 1'
29 | label = 'Mock Processor 1'
30 | tags = ['mock-processor1', 'test']
31 | attributes = {'test': True}
32 |
33 | @staticmethod
34 | def run(context: MockProcessor1Context):
35 | if context.intentional_error:
36 | raise Exception('Received intentional error parameter')
37 |
38 | print('Start mock processor1 in mock app')
39 | assert context.text1 != ''
40 | assert context.input_file.get_url()
41 | assert len(context.input_list) > 0
42 | for input_file in context.input_list:
43 | assert input_file.get_url()
44 | print(f'text1: {context.text1}')
45 | print(f'text2: {context.text2}')
46 | print(f'text3: {context.text3}')
47 | print(f'val1: {context.val1}')
48 | print(f'group.num: {context.group.num}')
49 | time.sleep(0.001) # important not to wait too long because we are calling this synchronously during testing
50 | with open('mock-output-file.txt', 'w') as f:
51 | f.write('mock output')
52 | context.output_file.upload('mock-output-file.txt')
53 | print('End mock processor1 in mock app')
54 |
55 | app = App(
56 | name='test-app',
57 | description='This is a test app',
58 | app_image=None,
59 | app_executable=os.path.abspath(__file__)
60 | )
61 |
62 | app.add_processor(MockProcessor1)
63 |
64 | if __name__ == '__main__':
65 | app.run()
66 |
--------------------------------------------------------------------------------
/python/tests/mock_app_2/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 | from dendro import BaseModel, Field
5 | from dendro.sdk import App, ProcessorBase
6 |
7 |
8 | class MockProcessor2Context(BaseModel):
9 | text1: str = Field(description='Text 1')
10 |
11 | class MockProcessor2(ProcessorBase):
12 | name = 'mock-processor2'
13 | description = 'This is mock processor 2'
14 | label = 'Mock Processor 2'
15 | tags = ['mock-processor2', 'test']
16 | attributes = {'test': True}
17 |
18 | @staticmethod
19 | def run(context: MockProcessor2Context):
20 | print('Start mock processor2 in mock app')
21 | assert context.text1 != ''
22 | print('End mock processor2 in mock app')
23 |
24 | app = App(
25 | name='test-app-2',
26 | description='This is a test app 2',
27 | app_image=None,
28 | app_executable=os.path.abspath(__file__)
29 | )
30 |
31 | app.add_processor(MockProcessor2)
32 |
33 | if __name__ == '__main__':
34 | app.run()
35 |
--------------------------------------------------------------------------------
/python/tests/test_api_request_failures.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from dendro.common._api_request import _processor_get_api_request, _processor_put_api_request
3 | from dendro.common._api_request import _client_get_api_request
4 | from dendro.common._api_request import _gui_get_api_request, _gui_put_api_request, _gui_post_api_request, _gui_delete_api_request
5 |
6 | @pytest.mark.api
7 | def test_api_request_failures():
8 | from dendro.common._api_request import _use_api_test_client
9 | from dendro.mock import set_use_mock
10 | from test_integration import _get_fastapi_app
11 | from dendro.api_helpers.clients._get_mongo_client import _clear_mock_mongo_databases
12 |
13 | from fastapi.testclient import TestClient
14 | app = _get_fastapi_app()
15 | test_client = TestClient(app)
16 | _use_api_test_client(test_client)
17 | set_use_mock(True)
18 |
19 | try:
20 | # from requests import exceptions
21 | with pytest.raises(Exception):
22 | _processor_get_api_request(url_path='/api/incorrect', headers={})
23 | with pytest.raises(Exception):
24 | _processor_put_api_request(url_path='/api/incorrect', headers={}, data={})
25 | with pytest.raises(Exception):
26 | _client_get_api_request(url_path='/api/incorrect')
27 | with pytest.raises(Exception):
28 | _gui_get_api_request(url_path='/api/incorrect', github_access_token='incorrect')
29 | with pytest.raises(Exception):
30 | _gui_put_api_request(url_path='/api/incorrect', github_access_token='incorrect', data={})
31 | with pytest.raises(Exception):
32 | _gui_post_api_request(url_path='/api/incorrect', github_access_token='incorrect', data={})
33 | with pytest.raises(Exception):
34 | _gui_delete_api_request(url_path='/api/incorrect', github_access_token='incorrect')
35 | finally:
36 | _use_api_test_client(None)
37 | set_use_mock(False)
38 | _clear_mock_mongo_databases()
39 |
--------------------------------------------------------------------------------
/python/tests/test_client.py:
--------------------------------------------------------------------------------
1 | import os
2 | from dendro.client import load_project
3 |
4 |
5 | def test_load_project():
6 | if os.getenv('NO_INTERNET') == '1':
7 | print('Skipping test_load_project because NO_INTERNET is set to 1')
8 | return
9 |
10 | # if this project or file disappears then we'll need to update that here
11 | project_id = 'eb87e88a'
12 | file_name = 'imported/000618/sub-paired-english/sub-paired-english_ses-paired-english-m108-191125-163508_ecephys.nwb'
13 | project = load_project(project_id)
14 | file = project.get_file(file_name)
15 | assert file is not None
16 | assert file.get_url().startswith('https://')
17 |
18 | folder = project.get_folder('imported')
19 | assert len(folder.get_folders()) >= 1
20 |
--------------------------------------------------------------------------------
/python/tests/test_compute_resource.py:
--------------------------------------------------------------------------------
1 | import shutil
2 | import tempfile
3 | from dendro.compute_resource.register_compute_resource import register_compute_resource
4 |
5 |
6 | def test_register_compute_resource():
7 | with TemporaryDirectory() as tmpdir:
8 | register_compute_resource(
9 | dir=tmpdir
10 | )
11 |
12 | class TemporaryDirectory:
13 | """A context manager for temporary directories"""
14 | def __init__(self):
15 | self._dir = None
16 | def __enter__(self):
17 | self._dir = tempfile.mkdtemp()
18 | return self._dir
19 | def __exit__(self, exc_type, exc_value, traceback):
20 | if self._dir:
21 | shutil.rmtree(self._dir)
22 |
--------------------------------------------------------------------------------
/python/tests/test_crypto_keys.py:
--------------------------------------------------------------------------------
1 | from dendro.api_helpers.services._crypto_keys import generate_keypair
2 |
3 |
4 | def test_crypto_keys():
5 | public_key_hex, private_key_hex = generate_keypair() # this does checks internally
6 | assert public_key_hex
7 | assert private_key_hex
8 |
--------------------------------------------------------------------------------
/python/tests/test_github_auth_route.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import os
3 |
4 |
5 | @pytest.mark.asyncio
6 | @pytest.mark.api
7 | async def test_github_auth():
8 | from dendro.api_helpers.routers.gui.github_auth_routes import github_auth, GithubAuthError
9 | old_env = os.environ.copy()
10 | try:
11 | os.environ['VITE_GITHUB_CLIENT_ID'] = 'test-client-id'
12 | os.environ['GITHUB_CLIENT_SECRET'] = 'test-client-secret'
13 | with pytest.raises(GithubAuthError):
14 | await github_auth(code='test-code')
15 | finally:
16 | os.environ = old_env
17 |
--------------------------------------------------------------------------------
/python/tests/test_sdk.py:
--------------------------------------------------------------------------------
1 | import shutil
2 | import tempfile
3 | from dendro import BaseModel, Field
4 | from dendro.sdk import App, ProcessorBase, InputFile, OutputFile
5 | from dendro.mock import set_use_mock
6 |
7 |
8 | def test_app():
9 | set_use_mock(True)
10 |
11 | try:
12 | class Processor1Context(BaseModel):
13 | input_file: InputFile = Field(description='Input file')
14 | output_file: OutputFile = Field(description='Output file')
15 | text1: str = Field(description='Text 1', default='abc')
16 | text2: str = Field(description='Text 2')
17 | text3: str = Field(description='Text 3', default='xyz', json_schema_extra={'options': ['abc', 'xyz']})
18 | val1: float = Field(description='Value 1', default=1.0)
19 |
20 | class Processor1(ProcessorBase):
21 | name = 'processor1'
22 | description = 'This is processor 1'
23 | label = 'Processor 1'
24 | tags = ['processor1', 'test']
25 | attributes = {'test': True}
26 |
27 | @staticmethod
28 | def run(context: Processor1Context):
29 | assert context.text1 != ''
30 |
31 | app = App(
32 | name='test-app',
33 | description='This is a test app',
34 | app_image='fake-image'
35 | )
36 |
37 | app.add_processor(Processor1)
38 |
39 | spec = app.get_spec()
40 | assert spec['name'] == 'test-app'
41 | finally:
42 | set_use_mock(False)
43 |
44 | class TemporaryDirectory:
45 | """A context manager for temporary directories"""
46 | def __init__(self):
47 | self._dir = None
48 | def __enter__(self):
49 | self._dir = tempfile.mkdtemp()
50 | return self._dir
51 | def __exit__(self, exc_type, exc_value, traceback):
52 | if self._dir:
53 | shutil.rmtree(self._dir)
54 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # NOTE: these are the requirements for the api, picked up by vercel
2 | fastapi
3 | motor
4 | simplejson
5 | # was having some trouble with the latest version of pydantic, so pinning it for now
6 | # error was: "No module named 'pydantic_core._pydantic_core'"
7 | pydantic==2.4
8 | aiohttp
9 | cryptography
10 | boto3
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | /* #root {
2 | max-width: 1280px;
3 | margin: 0 auto;
4 | padding: 2rem;
5 | text-align: center;
6 | }
7 |
8 | .logo {
9 | height: 6em;
10 | padding: 1.5em;
11 | will-change: filter;
12 | transition: filter 300ms;
13 | }
14 | .logo:hover {
15 | filter: drop-shadow(0 0 2em #646cffaa);
16 | }
17 | .logo.react:hover {
18 | filter: drop-shadow(0 0 2em #61dafbaa);
19 | }
20 |
21 | @keyframes logo-spin {
22 | from {
23 | transform: rotate(0deg);
24 | }
25 | to {
26 | transform: rotate(360deg);
27 | }
28 | }
29 |
30 | @media (prefers-reduced-motion: no-preference) {
31 | a:nth-of-type(2) .logo {
32 | animation: logo-spin infinite 20s linear;
33 | }
34 | }
35 |
36 | .card {
37 | padding: 2em;
38 | }
39 |
40 | .read-the-docs {
41 | color: #888;
42 | } */
43 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { BrowserRouter } from 'react-router-dom'
2 | import './App.css'
3 | import MainWindow from './MainWindow'
4 | import GithubAuthSetup from './GithubAuth/GithubAuthSetup'
5 | import { SetupDendro } from './DendroContext/DendroContext'
6 |
7 | function App() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | )
17 | }
18 |
19 | export default App
20 |
--------------------------------------------------------------------------------
/src/ComputeResourceNameDisplay.tsx:
--------------------------------------------------------------------------------
1 | import { Hyperlink } from "@fi-sci/misc";
2 | import { FunctionComponent, useMemo } from "react";
3 | import { useComputeResources } from "./pages/ComputeResourcesPage/ComputeResourcesContext";
4 | import useRoute from "./useRoute";
5 |
6 | type Props = {
7 | computeResourceId: string | undefined
8 | link?: boolean
9 | }
10 |
11 | const ComputeResourceNameDisplay: FunctionComponent = ({ computeResourceId, link }) => {
12 | const {computeResources} = useComputeResources()
13 | const displayString = useMemo(() => {
14 | if (!computeResourceId) return 'DEFAULT'
15 | const cr = computeResources.find(cr => cr.computeResourceId === computeResourceId)
16 | return (cr ? cr.name + ` (${abbreviate(computeResourceId, 9)})` : undefined) || abbreviate(computeResourceId, 16)
17 | }, [computeResources, computeResourceId])
18 | const {setRoute} = useRoute()
19 | const a = {displayString || ''}
20 | const crId = computeResourceId || import.meta.env.VITE_DEFAULT_COMPUTE_RESOURCE_ID
21 | if (link) {
22 | return setRoute({page: 'compute-resource', computeResourceId: crId})}>{a}
23 | }
24 | else {
25 | return a
26 | }
27 | }
28 |
29 | function abbreviate(s: string, maxLength: number) {
30 | if (s.length <= maxLength) return s
31 | return s.slice(0, maxLength - 3) + '...'
32 | }
33 |
34 | export default ComputeResourceNameDisplay
--------------------------------------------------------------------------------
/src/DendroContext/DendroContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent, PropsWithChildren, useCallback, useMemo } from 'react';
2 | import { DendroProject } from '../types/dendro-types';
3 |
4 | type Props = {
5 | // none
6 | }
7 |
8 | type DendroState = {
9 | loadedProjects: DendroProject[]
10 | }
11 |
12 | type DendroAction = {
13 | type: 'reportLoadedProject'
14 | project: DendroProject
15 | }
16 |
17 | const dendroReducer = (state: DendroState, action: DendroAction) => {
18 | switch (action.type) {
19 | case 'reportLoadedProject':
20 | return {
21 | ...state,
22 | loadedProjects: [...state.loadedProjects.filter(x => x.projectId !== action.project.projectId), action.project]
23 | }
24 | }
25 | }
26 |
27 | type DendroContextType = {
28 | loadedProjects: DendroProject[]
29 | reportLoadedProject: (project: DendroProject) => void
30 | }
31 |
32 | const DendroContext = React.createContext({
33 | loadedProjects: [],
34 | reportLoadedProject: () => {}
35 | })
36 |
37 | export const SetupDendro: FunctionComponent> = ({children}) => {
38 | const [state, dispatch] = React.useReducer(dendroReducer, {
39 | loadedProjects: []
40 | })
41 |
42 | const value = {
43 | loadedProjects: state.loadedProjects,
44 | reportLoadedProject: useCallback((project: DendroProject) => {
45 | dispatch({
46 | type: 'reportLoadedProject',
47 | project
48 | })
49 | }, [])
50 | }
51 |
52 | return (
53 |
54 | {children}
55 |
56 | )
57 | }
58 |
59 | export const useDendro = () => {
60 | return React.useContext(DendroContext)
61 | }
--------------------------------------------------------------------------------
/src/GitHub/GitHubAuthPage.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, useEffect, useState } from "react";
2 | import { setGitHubTokenInfoToLocalStorage } from "../GithubAuth/getGithubAuthFromLocalStorage";
3 | import { apiBase } from "../dbInterface/dbInterface";
4 |
5 | type Props = any
6 |
7 | function parseQuery(queryString: string) {
8 | const ind = queryString.indexOf('?')
9 | if (ind <0) return {}
10 | const query: {[k: string]: string} = {};
11 | const pairs = queryString.slice(ind + 1).split('&');
12 | for (let i = 0; i < pairs.length; i++) {
13 | const pair = pairs[i].split('=');
14 | query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
15 | }
16 | return query;
17 | }
18 |
19 | // Important to do it this way because it is difficult to handle special characters (especially #) by using URLSearchParams or window.location.search
20 | const queryParams = parseQuery(window.location.href)
21 |
22 | const GitHubAuthPage: FunctionComponent = () => {
23 | const [status, setStatus] = useState<'checking' | 'okay' | 'error'>('checking')
24 | const [error, setError] = useState('')
25 | const code = queryParams.code
26 | useEffect(() => {
27 | (async () => {
28 | const rr = await fetch(`${apiBase}/api/gui/github_auth/${code}`)
29 | const r = await rr.json()
30 | if ((!r.access_token) || (r.error)) {
31 | setStatus('error')
32 | setError(r.error)
33 | return
34 | }
35 | setGitHubTokenInfoToLocalStorage({
36 | token: r.access_token,
37 | isPersonalAccessToken: false
38 | })
39 | setStatus('okay')
40 | })()
41 | }, [code])
42 | return (
43 |
44 | {
45 | status === 'checking' ? (
46 |
Checking authorization
47 | ) : status === 'okay' ? (
48 |
Logged in. You may now close this window.
49 | ) : status === 'error' ? (
50 |
Error: {error}
51 | ) : (
52 |
Unexpected status: {status}
53 | )
54 | }
55 |
56 | )
57 | }
58 |
59 | export default GitHubAuthPage
60 |
--------------------------------------------------------------------------------
/src/GitHub/PersonalAccessTokenWindow.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Input } from "@mui/material";
2 | import { FunctionComponent, useCallback, useState } from "react";
3 | import { getGitHubTokenInfoFromLocalStorage, setGitHubTokenInfoToLocalStorage } from "../GithubAuth/getGithubAuthFromLocalStorage";
4 |
5 | type Props ={
6 | onChange: () => void
7 | }
8 |
9 | const PersonalAccessTokenWindow: FunctionComponent = ({onChange}) => {
10 | const [newToken, setNewToken] = useState('')
11 | const handleNewTokenChange: React.ChangeEventHandler = useCallback((e) => {
12 | setNewToken(e.target.value as string)
13 | }, [])
14 | const handleSubmit = useCallback(() => {
15 | setGitHubTokenInfoToLocalStorage({token: newToken, isPersonalAccessToken: true})
16 | setNewToken('')
17 | onChange()
18 | }, [newToken, onChange])
19 |
20 | const oldTokenInfo = getGitHubTokenInfoFromLocalStorage()
21 |
22 | return (
23 |
24 |
25 | To write to public GitHub repositories and to read and write from private GitHub repositories
26 | you will need to set a GitHub access token. This token will be stored in the local storage of your browser.
27 | You should create a personal access token
28 | with the least amount of permissions needed.
29 |
30 |
31 | Use the classic type personal access token with repo scope.
32 |
33 |
34 |
35 | {
36 | newToken && (
37 |
Submit
38 | )
39 | }
40 | {
41 | oldTokenInfo?.token ? (
42 |
43 | GitHub access token has been set.
44 |
45 | ) : (
46 |
47 | GitHub access token not set.
48 |
49 | )
50 | }
51 |
52 | )
53 | }
54 |
55 | export default PersonalAccessTokenWindow
56 |
--------------------------------------------------------------------------------
/src/GithubAuth/GithubAuthContext.ts:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export type GithubAuthData = {
4 | signedIn: boolean
5 | userId?: string,
6 | accessToken?: string
7 | isPersonalAccessToken?: boolean
8 | clearAccessToken: () => void
9 | loginStatus?: 'not-logged-in' | 'checking' | 'logged-in'
10 | }
11 |
12 | const dummyGithubAuthData: GithubAuthData = {signedIn: false, clearAccessToken: () => {}}
13 |
14 | const GithubAuthContext = React.createContext(dummyGithubAuthData)
15 |
16 | export default GithubAuthContext
--------------------------------------------------------------------------------
/src/GithubAuth/GithubAuthSetup.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent, PropsWithChildren } from 'react';
2 | import GithubAuthContext from './GithubAuthContext';
3 | import useSetupGithubAuth from './useSetupGithubAuth';
4 |
5 | const GithubAuthSetup: FunctionComponent = (props) => {
6 | const githubAuthData = useSetupGithubAuth()
7 | return (
8 |
9 | {props.children}
10 |
11 | )
12 | }
13 |
14 | export default GithubAuthSetup
--------------------------------------------------------------------------------
/src/GithubAuth/getGithubAuthFromLocalStorage.ts:
--------------------------------------------------------------------------------
1 | import validateObject, { isBoolean, isNumber, isString, optional } from "../types/validateObject"
2 |
3 | export type GitHubTokenInfo = {
4 | token?: string
5 | userId?: string
6 | userIdTimestamp?: number
7 | isPersonalAccessToken?: boolean
8 | }
9 |
10 | export const isGithubTokenInfo = (x: any): x is GitHubTokenInfo => {
11 | return validateObject(x, {
12 | token: optional(isString),
13 | userId: optional(isString),
14 | userIdTimestamp: optional(isNumber),
15 | isPersonalAccessToken: optional(isBoolean)
16 | })
17 | }
18 |
19 | export const setGitHubTokenInfoToLocalStorage = (tokenInfo: GitHubTokenInfo) => {
20 | localStorage.setItem('githubToken', JSON.stringify(tokenInfo))
21 | }
22 |
23 | export const getGitHubTokenInfoFromLocalStorage = (): GitHubTokenInfo | undefined => {
24 | const a = localStorage.getItem('githubToken')
25 | if (!a) return undefined
26 | try {
27 | const b = JSON.parse(a)
28 | if (isGithubTokenInfo(b)) {
29 | return b
30 | }
31 | else {
32 | console.warn(b)
33 | console.warn('Invalid GitHub token info.')
34 | localStorage.removeItem('githubToken')
35 | return undefined
36 | }
37 | }
38 | catch {
39 | console.warn(a)
40 | console.warn('Error with github token info.')
41 | return undefined
42 | }
43 | }
--------------------------------------------------------------------------------
/src/GithubAuth/useGithubAuth.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from "react"
2 | import GithubAuthContext from "./GithubAuthContext"
3 |
4 | export const useGithubAuth = () => {
5 | return useContext(GithubAuthContext)
6 | }
--------------------------------------------------------------------------------
/src/RemoteH5File/h5wasm/file_handlers.js:
--------------------------------------------------------------------------------
1 | import { ready } from "./hdf5_hl.js";
2 | export const UPLOADED_FILES = [];
3 | export async function uploader(event) {
4 | const { FS } = await ready;
5 | const target = event.target;
6 | let file = target.files?.[0]; // only one file allowed
7 | if (file) {
8 | let datafilename = file.name;
9 | let ab = await file.arrayBuffer();
10 | FS.writeFile(datafilename, new Uint8Array(ab));
11 | if (!UPLOADED_FILES.includes(datafilename)) {
12 | UPLOADED_FILES.push(datafilename);
13 | console.log("file loaded:", datafilename);
14 | }
15 | else {
16 | console.log("file updated: ", datafilename);
17 | }
18 | target.value = "";
19 | }
20 | }
21 | function create_downloader() {
22 | let a = document.createElement("a");
23 | document.body.appendChild(a);
24 | a.style.display = "none";
25 | a.id = "savedata";
26 | return function (data, fileName) {
27 | let blob = (data instanceof Blob) ? data : new Blob([data], { type: 'application/x-hdf5' });
28 | // IE 10 / 11
29 | const nav = window.navigator;
30 | if (nav.msSaveOrOpenBlob) {
31 | nav.msSaveOrOpenBlob(blob, fileName);
32 | }
33 | else {
34 | let url = window.URL.createObjectURL(blob);
35 | a.href = url;
36 | a.download = fileName;
37 | a.target = "_blank";
38 | //window.open(url, '_blank', fileName);
39 | a.click();
40 | setTimeout(function () { window.URL.revokeObjectURL(url); }, 1000);
41 | }
42 | // cleanup: this seems to break things!
43 | //document.body.removeChild(a);
44 | };
45 | }
46 | ;
47 | export const downloader = create_downloader();
48 | export async function to_blob(hdf5_file) {
49 | const { FS } = await ready;
50 | hdf5_file.flush();
51 | return new Blob([FS.readFile(hdf5_file.filename)], { type: 'application/x-hdf5' });
52 | }
53 | export async function download(hdf5_file) {
54 | let b = await to_blob(hdf5_file);
55 | downloader(b, hdf5_file.filename);
56 | }
57 | export function dirlisting(path, FS) {
58 | let node = FS.analyzePath(path).object;
59 | if (node && node.isFolder) {
60 | let files = Object.values(node.contents).filter(v => !(v.isFolder)).map(v => v.name);
61 | let subfolders = Object.values(node.contents).filter(v => (v.isFolder)).map(v => v.name);
62 | return { files, subfolders };
63 | }
64 | else {
65 | return {};
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/RemoteH5File/h5wasm/readme.txt:
--------------------------------------------------------------------------------
1 | The files in this directory were taken from a transpiled snapshot of h5wasm
2 | https://github.com/usnistgov/h5wasm
3 |
4 | Various modifications were made in hdf5_util_jfm.js to support specification
5 | of the chunk size, which is important for performance reasons.
6 |
7 | Another modification was to extract out the wasm binary program into a separate
8 | file (wasmBinaryFile.js). This was done to avoid including multiple modified
9 | versions of a large file (3.2M) in the git repo. The idea being that
10 | wasmBinaryFile.js should not change over a relatively long period of time,
11 | whereas hdf5_util_jfm.js may be tweaked more often.
12 |
13 | I am sure there's a better way to deal with this situation, but this is what
14 | I'm doing for now.
15 |
16 | - Jeremy 7/23/2023
--------------------------------------------------------------------------------
/src/TabWidget/TabWidgetTabBar.tsx:
--------------------------------------------------------------------------------
1 | import { Tab, Tabs } from '@mui/material';
2 | import { FunctionComponent, useEffect } from 'react';
3 |
4 | type Props = {
5 | tabs: {
6 | id: string
7 | label: string
8 | closeable: boolean
9 | icon?: any
10 | title?: string
11 | }[]
12 | currentTabIndex: number | undefined
13 | onCurrentTabIndexChanged: (i: number) => void
14 | onCloseTab: (id: string) => void
15 | }
16 |
17 | const TabWidgetTabBar: FunctionComponent = ({ tabs, currentTabIndex, onCurrentTabIndexChanged, onCloseTab }) => {
18 | useEffect(() => {
19 | if (currentTabIndex === undefined) {
20 | if (tabs.length > 0) {
21 | onCurrentTabIndexChanged(0)
22 | }
23 | }
24 | }, [currentTabIndex, onCurrentTabIndexChanged, tabs.length])
25 | return (
26 | {onCurrentTabIndexChanged(value)}}
31 | >
32 | {tabs.map((tab, i) => (
33 |
36 | {
37 | tab.icon ? (
38 | {tab.icon}
39 | ) :
40 | }
41 | {tab.label}
42 | {onCloseTab(tab.id)}}>✕
43 |
44 | ) : (
45 | tab.label
46 | )
47 | } sx={{minHeight: 0, height: 0, fontSize: 12}} />
48 | ))}
49 |
50 | )
51 | }
52 |
53 | export default TabWidgetTabBar
--------------------------------------------------------------------------------
/src/UserIdComponent.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from "react";
2 |
3 | type Props = {
4 | userId: string | undefined
5 | }
6 |
7 | const UserIdComponent: FunctionComponent = ({ userId }) => {
8 | const x = userId ? (userId.startsWith('github|') ? userId.slice('github|'.length) : userId) : ''
9 | return {x}
10 | }
11 |
12 | export default UserIdComponent
--------------------------------------------------------------------------------
/src/confirm_prompt_alert.ts:
--------------------------------------------------------------------------------
1 | export const confirm = async (message: string): Promise => {
2 | return window.confirm(message)
3 | }
4 |
5 | export const prompt = async (message: string, defaultValue: string): Promise => {
6 | return window.prompt(message, defaultValue)
7 | }
8 |
9 | export const alert = async (message: string): Promise => {
10 | return window.alert(message)
11 | }
--------------------------------------------------------------------------------
/src/dbInterface/getAuthorizationHeaderForUrl.ts:
--------------------------------------------------------------------------------
1 | const getAuthorizationHeaderForUrl = (url?: string) => {
2 | if (!url) return ''
3 | let key = ''
4 | if (url.startsWith('https://api-staging.dandiarchive.org/')) {
5 | key = localStorage.getItem('dandiStagingApiKey') || ''
6 | }
7 | else if (url.startsWith('https://api.dandiarchive.org/')) {
8 | key = localStorage.getItem('dandiApiKey') || ''
9 | }
10 | if (key) return 'token ' + key
11 | else return ''
12 | }
13 |
14 | export default getAuthorizationHeaderForUrl
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: 'Arial', sans-serif;
4 | }
5 |
6 | /* :root {
7 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
8 | line-height: 1.5;
9 | font-weight: 400;
10 |
11 | color-scheme: light dark;
12 | color: rgba(255, 255, 255, 0.87);
13 | background-color: #242424;
14 |
15 | font-synthesis: none;
16 | text-rendering: optimizeLegibility;
17 | -webkit-font-smoothing: antialiased;
18 | -moz-osx-font-smoothing: grayscale;
19 | -webkit-text-size-adjust: 100%;
20 | }
21 |
22 | a {
23 | font-weight: 500;
24 | color: #646cff;
25 | text-decoration: inherit;
26 | }
27 | a:hover {
28 | color: #535bf2;
29 | }
30 |
31 | body {
32 | margin: 0;
33 | display: flex;
34 | place-items: center;
35 | min-width: 320px;
36 | min-height: 100vh;
37 | }
38 |
39 | h1 {
40 | font-size: 3.2em;
41 | line-height: 1.1;
42 | }
43 |
44 | button {
45 | border-radius: 8px;
46 | border: 1px solid transparent;
47 | padding: 0.6em 1.2em;
48 | font-size: 1em;
49 | font-weight: 500;
50 | font-family: inherit;
51 | background-color: #1a1a1a;
52 | cursor: pointer;
53 | transition: border-color 0.25s;
54 | }
55 | button:hover {
56 | border-color: #646cff;
57 | }
58 | button:focus,
59 | button:focus-visible {
60 | outline: 4px auto -webkit-focus-ring-color;
61 | }
62 |
63 | @media (prefers-color-scheme: light) {
64 | :root {
65 | color: #213547;
66 | background-color: #ffffff;
67 | }
68 | a:hover {
69 | color: #747bff;
70 | }
71 | button {
72 | background-color: #f9f9f9;
73 | }
74 | } */
75 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client'
2 | import App from './App'
3 | import './index.css'
4 | import './table1.css'
5 | import './scientific-table.css'
6 |
7 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
8 | //
9 |
10 | // ,
11 | )
12 |
--------------------------------------------------------------------------------
/src/pages/AboutPage/AboutPage.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from "react";
2 |
3 | type Props = {
4 | width: number
5 | height: number
6 | }
7 |
8 | const AboutPage: FunctionComponent = ({width, height}) => {
9 | return (
10 |
11 | This is dendro.
12 |
13 | )
14 | }
15 |
16 | export default AboutPage
--------------------------------------------------------------------------------
/src/pages/AdminPage/AdminPage.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from "react"
2 | import ProjectsTable from "../ProjectsPage/ProjectsTable"
3 |
4 | type AdminPageProps = {
5 | width: number
6 | height: number
7 | }
8 |
9 | const AdminPage: FunctionComponent = ({width, height}) => {
10 | return (
11 |
14 | )
15 | }
16 |
17 | export default AdminPage
--------------------------------------------------------------------------------
/src/pages/ComputeResourcePage/ComputeResourceAppsTableMenuBar.tsx:
--------------------------------------------------------------------------------
1 | import { Add, Delete, Edit } from "@mui/icons-material"
2 | import { FunctionComponent, useCallback } from "react"
3 | import { SmallIconButton } from "@fi-sci/misc";
4 | import { confirm } from "../../confirm_prompt_alert"
5 |
6 | type ComputeResourceAppsTableMenuBarProps = {
7 | width: number
8 | height: number
9 | selectedAppNames: string[]
10 | onDeleteApps: (appNames: string[]) => void
11 | onAddApp: () => void
12 | onEditApp: () => void
13 | }
14 |
15 | const ComputeResourceAppsTableMenuBar: FunctionComponent = ({width, height, selectedAppNames, onAddApp, onDeleteApps, onEditApp}) => {
16 | const handleDelete = useCallback(async () => {
17 | const okay = await confirm(`Are you sure you want to delete these ${selectedAppNames.length} apps?`)
18 | if (!okay) return
19 | onDeleteApps(selectedAppNames)
20 | }, [selectedAppNames, onDeleteApps])
21 |
22 | return (
23 |
24 |
25 | }
27 | title={"Add an app"}
28 | label="Add app"
29 | onClick={onAddApp}
30 | />
31 |
32 |
33 | }
35 | disabled={(selectedAppNames.length === 0)}
36 | title={selectedAppNames.length > 0 ? `Delete these ${selectedAppNames.length} apps` : ''}
37 | onClick={handleDelete}
38 | />
39 |
40 | }
42 | disabled={(selectedAppNames.length !== 1)}
43 | title={selectedAppNames.length === 1 ? `Edit app ${selectedAppNames[0]}` : ''}
44 | onClick={onEditApp}
45 | />
46 |
47 | )
48 | }
49 |
50 | export default ComputeResourceAppsTableMenuBar
--------------------------------------------------------------------------------
/src/pages/ComputeResourcesPage/ComputeResourcesContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent, PropsWithChildren, useCallback, useEffect, useMemo, useState } from 'react';
2 | import { deleteComputeResource, fetchComputeResources } from '../../dbInterface/dbInterface';
3 | import { useGithubAuth } from '../../GithubAuth/useGithubAuth';
4 | import { DendroComputeResource } from '../../types/dendro-types';
5 |
6 | type Props = {
7 | // none
8 | }
9 |
10 | type ComputeResourcesContextType = {
11 | computeResources: DendroComputeResource[]
12 | refreshComputeResources: () => void
13 | deleteComputeResource: (computeResourceId: string) => void
14 | }
15 |
16 | const ComputeResourcesContext = React.createContext({
17 | computeResources: [],
18 | refreshComputeResources: () => {},
19 | deleteComputeResource: () => {}
20 | })
21 |
22 | export const SetupComputeResources: FunctionComponent> = ({children}) => {
23 | const [computeResources, setComputeResources] = useState([])
24 | const [refreshComputeResourcesCode, setRefreshComputeResourcesCode] = useState(0)
25 | const refreshComputeResources = useCallback(() => setRefreshComputeResourcesCode(rc => rc + 1), [])
26 |
27 | const auth = useGithubAuth()
28 |
29 | useEffect(() => {
30 | (async () => {
31 | setComputeResources([])
32 | if (!auth) return
33 | const cr = await fetchComputeResources(auth)
34 | setComputeResources(cr)
35 | })()
36 | }, [auth, refreshComputeResourcesCode])
37 |
38 | const deleteComputeResourceHandler = useCallback(async (computeResourceId: string) => {
39 | if (!auth) return
40 | await deleteComputeResource(computeResourceId, auth)
41 | refreshComputeResources()
42 | }, [auth, refreshComputeResources])
43 |
44 | const value = React.useMemo(() => ({
45 | computeResources,
46 | refreshComputeResources,
47 | deleteComputeResource: deleteComputeResourceHandler
48 | }), [computeResources, refreshComputeResources, deleteComputeResourceHandler])
49 |
50 | return (
51 |
52 | {children}
53 |
54 | )
55 | }
56 |
57 | export const useComputeResources = () => {
58 | const context = React.useContext(ComputeResourcesContext)
59 | return context
60 | }
--------------------------------------------------------------------------------
/src/pages/ComputeResourcesPage/ComputeResourcesPage.css:
--------------------------------------------------------------------------------
1 | .compute-resources-page {
2 | padding: 20px;
3 | font-family: Arial, sans-serif;
4 | max-width: 800px;
5 | margin: auto;
6 | }
7 |
8 | .compute-resources-page h1, .compute-resources-page h2 {
9 | color: #333;
10 | text-align: center;
11 | }
12 |
13 | .compute-resources-page p {
14 | margin-bottom: 20px;
15 | text-align: justify;
16 | }
--------------------------------------------------------------------------------
/src/pages/ComputeResourcesPage/ComputeResourcesPage.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from "react";
2 | import { SetupComputeResources } from "./ComputeResourcesContext";
3 | import ComputeResourcesTable from "./ComputeResourcesTable";
4 | import './ComputeResourcesPage.css'
5 |
6 | type Props = {
7 | width: number
8 | height: number
9 | }
10 |
11 | const ComputeResourcesPage: FunctionComponent = ({width, height}) => {
12 | return (
13 |
14 |
24 |
25 | )
26 | }
27 |
28 | export default ComputeResourcesPage
--------------------------------------------------------------------------------
/src/pages/ComputeResourcesPage/ComputeResourcesTable.tsx:
--------------------------------------------------------------------------------
1 | import { Delete } from "@mui/icons-material"
2 | import { FunctionComponent, useCallback } from "react"
3 | import { Hyperlink } from "@fi-sci/misc";
4 | import ComputeResourceNameDisplay from "../../ComputeResourceNameDisplay"
5 | import { confirm } from "../../confirm_prompt_alert"
6 | import { timeAgoString } from "../../timeStrings"
7 | import UserIdComponent from "../../UserIdComponent"
8 | import useRoute from "../../useRoute"
9 | import { useComputeResources } from "./ComputeResourcesContext"
10 |
11 | type Props = {
12 | // none
13 | }
14 |
15 | const ComputeResourcesTable: FunctionComponent = () => {
16 | const {computeResources, deleteComputeResource} = useComputeResources()
17 |
18 | const handleDeleteComputeResource = useCallback(async (computeResourceId: string) => {
19 | const okay = await confirm('Are you sure you want to delete this compute resource?')
20 | if (!okay) return
21 | deleteComputeResource(computeResourceId)
22 | }, [deleteComputeResource])
23 |
24 | const { setRoute } = useRoute()
25 |
26 | return (
27 |
28 |
29 |
30 |
31 | Compute resource
32 | ID
33 | Owner
34 | Created
35 |
36 |
37 |
38 | {
39 | computeResources.map((cr) => (
40 |
41 | handleDeleteComputeResource(cr.computeResourceId)} />
42 |
43 | setRoute({page: 'compute-resource', computeResourceId: cr.computeResourceId})}>
44 | {cr.name}
45 |
46 |
47 |
48 | setRoute({page: 'compute-resource', computeResourceId: cr.computeResourceId})}>
49 |
50 |
51 |
52 |
53 | {timeAgoString(cr.timestampCreated)}
54 |
55 | ))
56 | }
57 |
58 |
59 | )
60 | }
61 |
62 | export default ComputeResourcesTable
--------------------------------------------------------------------------------
/src/pages/DandiBrowser/formatByteCount.ts:
--------------------------------------------------------------------------------
1 | const formatByteCount = (a: number) => {
2 | if (a < 10000) {
3 | return `${a} bytes`
4 | }
5 | else if (a < 100 * 1024) {
6 | return `${formatNum(a / 1024)} KB`
7 | }
8 | else if (a < 100 * 1024 * 1024) {
9 | return `${formatNum(a / (1024 * 1024))} MB`
10 | }
11 | else if (a < 600 * 1024 * 1024 * 1024) {
12 | return `${formatNum(a / (1024 * 1024 * 1025))} GB`
13 | }
14 | else {
15 | return `${formatNum(a / (1024 * 1024 * 1024 * 1024))} TB`
16 | }
17 | }
18 |
19 | export const formatGiBCount = (a: number) => {
20 | return formatByteCount(a * 1000 * 1000 * 1000)
21 | }
22 |
23 | const formatNum = (a: number) => {
24 | const b = a.toFixed(2)
25 | if (Number(b) - Math.floor(Number(b)) === 0) {
26 | return a.toFixed(0)
27 | }
28 | else return b
29 | }
30 |
31 | export default formatByteCount
--------------------------------------------------------------------------------
/src/pages/DandiBrowser/useProjectsForTag.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { DendroProject } from "../../types/dendro-types";
3 | import { useGithubAuth } from "../../GithubAuth/useGithubAuth";
4 | import { fetchProjectsForTag } from "../../dbInterface/dbInterface";
5 |
6 | const useProjectsForTag = (tag: string | undefined): DendroProject[] | undefined => {
7 | const [projects, setProjects] = useState(undefined)
8 | const auth = useGithubAuth()
9 | useEffect(() => {
10 | let canceled = false
11 |
12 | if (!tag) {
13 | setProjects([])
14 | return
15 | }
16 |
17 | setProjects(undefined)
18 |
19 | ; (async () => {
20 | const projects = await fetchProjectsForTag(tag, auth)
21 | if (canceled) return
22 | setProjects(projects)
23 | })()
24 | return () => {
25 | canceled = true
26 | }
27 | }, [tag, auth])
28 | return projects
29 | }
30 |
31 | export default useProjectsForTag
--------------------------------------------------------------------------------
/src/pages/HomePage/HomePage.css:
--------------------------------------------------------------------------------
1 | .HomePage {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | height: 100vh;
6 | font-size: 20px;
7 | }
8 |
9 | p {
10 | padding: 0px;
11 | margin: 7px;
12 | }
--------------------------------------------------------------------------------
/src/pages/HomePage/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, useEffect } from "react";
2 | import './HomePage.css'
3 | import { Hyperlink } from "@fi-sci/misc";
4 | import useRoute from "../../useRoute";
5 |
6 | type Props = {
7 | width: number
8 | height: number
9 | }
10 |
11 | const HomePage: FunctionComponent = ({width, height}) => {
12 | const {setRoute} = useRoute()
13 | return (
14 |
15 |
Welcome to Dendro!
16 |
17 | setRoute({page: 'dandisets'})}>
18 | Import from DANDI
19 |
20 |
21 |
22 | setRoute({page: 'projects'})}>
23 | Manage projects
24 |
25 |
26 |
27 | setRoute({page: 'compute-resources'})}>
28 | Configure compute
29 |
30 |
31 |
32 | {
33 | window.open('https://github.com/flatironinstitute/dendro', '_blank')
34 | }}>
35 | Star us on GitHub
36 |
37 |
38 |
39 | {
40 | window.open('https://flatironinstitute.github.io/dendro-docs/docs/intro', '_blank')
41 | }}>
42 | View the documentation
43 |
44 |
45 |
46 | )
47 | }
48 |
49 | export default HomePage
--------------------------------------------------------------------------------
/src/pages/ProjectPage/ComputeResourceAppsTable2/ComputeResourceAppsTableMenuBar2.tsx:
--------------------------------------------------------------------------------
1 | import { Add, Delete, Edit } from "@mui/icons-material"
2 | import { FunctionComponent, useCallback } from "react"
3 | import { SmallIconButton } from "@fi-sci/misc";
4 |
5 | type ComputeResourceAppsTableMenuBarProps = {
6 | selectedAppNames: string[]
7 | onDeleteApps: (appNames: string[]) => void
8 | onAddApp: () => void
9 | onEditApp: () => void
10 | }
11 |
12 | const ComputeResourceAppsTableMenuBar2: FunctionComponent = ({selectedAppNames, onAddApp, onDeleteApps, onEditApp}) => {
13 | const handleDelete = useCallback(async () => {
14 | const okay = await confirm(`Are you sure you want to delete these ${selectedAppNames.length} apps?`)
15 | if (!okay) return
16 | onDeleteApps(selectedAppNames)
17 | }, [selectedAppNames, onDeleteApps])
18 |
19 | return (
20 |
21 |
22 | }
24 | title={"Add an app"}
25 | label="Add app"
26 | onClick={onAddApp}
27 | />
28 |
29 |
30 | }
32 | disabled={(selectedAppNames.length === 0)}
33 | title={selectedAppNames.length > 0 ? `Delete these ${selectedAppNames.length} apps` : ''}
34 | onClick={handleDelete}
35 | />
36 |
37 | }
39 | disabled={(selectedAppNames.length !== 1)}
40 | title={selectedAppNames.length === 1 ? `Edit app ${selectedAppNames[0]}` : ''}
41 | onClick={onEditApp}
42 | />
43 |
44 | )
45 | }
46 |
47 | export default ComputeResourceAppsTableMenuBar2
--------------------------------------------------------------------------------
/src/pages/ProjectPage/ComputeResourceUsageComponent/ComputeResourceUsageComponent.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, useEffect, useState } from "react"
2 | import { ComputeResourceUserUsage, isComputeResourceUserUsage } from "../../../types/dendro-types"
3 | import { apiBase, getRequest } from "../../../dbInterface/dbInterface"
4 | import { useGithubAuth } from "../../../GithubAuth/useGithubAuth"
5 | import { GithubAuthData } from "../../../GithubAuth/GithubAuthContext"
6 |
7 | type ComputeResourceUsageComponentProps = {
8 | computeResourceId: string
9 | }
10 |
11 | const useComputeResourceUserUsage = (computeResourceId: string, userId: string | undefined, auth: GithubAuthData) => {
12 | const [usage, setUsage] = useState(undefined)
13 |
14 | useEffect(() => {
15 | if (!userId) return
16 | let canceled = false
17 | const fetchUsage = async () => {
18 | const url = `${apiBase}/api/gui/usage/compute_resource/${computeResourceId}/user/${userId}`
19 | const response = await getRequest(url, auth)
20 | if (canceled) return
21 | if (!response.success) {
22 | console.error("Error fetching usage", response)
23 | return
24 | }
25 | const u = response.usage
26 | if (!isComputeResourceUserUsage(u)) {
27 | console.error("Invalid usage", u)
28 | return
29 | }
30 | setUsage(u)
31 | }
32 | fetchUsage()
33 | return () => { canceled = true }
34 | }, [computeResourceId, userId, auth])
35 |
36 | return usage
37 | }
38 |
39 | const ComputeResourceUsageComponent: FunctionComponent = ({computeResourceId}) => {
40 | const auth = useGithubAuth()
41 | const usage = useComputeResourceUserUsage(computeResourceId, auth.userId, auth)
42 | if (!auth.userId) return Not logged in
43 | if (!usage) return Loading...
44 | const numJobsIncludingDeleted = usage.jobsIncludingDeleted.length
45 | const numJobs = usage.jobsIncludingDeleted.filter(j => (!j.deleted)).length
46 | return (
47 |
48 |
49 |
50 |
51 | Num. jobs
52 | {numJobs}
53 |
54 |
55 | Num. jobs including deleted
56 | {numJobsIncludingDeleted}
57 |
58 |
59 |
60 |
61 | )
62 | }
63 |
64 | export default ComputeResourceUsageComponent
--------------------------------------------------------------------------------
/src/pages/ProjectPage/DandiUpload/prepareDandiUploadTask.ts:
--------------------------------------------------------------------------------
1 | export type DandiUploadTask = {
2 | dandisetId: string
3 | dandiInstance: string
4 | fileNames: string[]
5 | names: string[]
6 | }
7 |
8 | const prepareDandiUploadTask = (fileNames: string[]): DandiUploadTask | undefined => {
9 | if (fileNames.length === 0) {
10 | return undefined
11 | }
12 | let topLevelDirectoryName: string | undefined = undefined
13 | let dandisetId: string | undefined = undefined
14 | let staging: boolean | undefined = undefined
15 | const names: string[] = []
16 | for (const fileName of fileNames) {
17 | if (!fileName.endsWith('.nwb')) {
18 | return undefined
19 | }
20 | const a = fileName.split('/')
21 | if (a.length <3) {
22 | return undefined
23 | }
24 | if ((topLevelDirectoryName) && (topLevelDirectoryName !== a[0])) {
25 | return undefined
26 | }
27 | const dd = a[1].startsWith('staging-') ? a[1].substring('staging-'.length) : a[1]
28 | if ((dandisetId) && (dandisetId !== dd)) {
29 | return undefined
30 | }
31 | const ss = a[1].startsWith('staging-')
32 | if ((staging !== undefined) && (staging !== ss)) {
33 | return undefined
34 | }
35 | topLevelDirectoryName = a[0]
36 | dandisetId = dd
37 | staging = ss
38 | names.push(a.slice(2).join('/'))
39 | }
40 | if ((!topLevelDirectoryName) || (!dandisetId) || (staging === undefined)) {
41 | return undefined
42 | }
43 | if (!validDandisetId(dandisetId)) {
44 | return undefined
45 | }
46 | if (topLevelDirectoryName !== 'generated') {
47 | return undefined
48 | }
49 | return {
50 | dandisetId,
51 | dandiInstance: staging ? 'dandi-staging' : 'dandi',
52 | fileNames,
53 | names
54 | }
55 | }
56 |
57 | const validDandisetId = (dandisetId: string): boolean => {
58 | // must be an integer
59 | if (!dandisetId.match(/^\d+$/)) {
60 | return false
61 | }
62 | return true
63 | }
64 |
65 | export default prepareDandiUploadTask
66 |
--------------------------------------------------------------------------------
/src/pages/ProjectPage/FileActions.tsx:
--------------------------------------------------------------------------------
1 | export type FileAction = {
2 | name: string
3 | label: string
4 | processorName: string
5 | outputFileBaseName: string
6 | icon: any
7 | }
8 |
9 | export const fileActions: FileAction[] = [
10 | // {
11 | // name: 'spike_sorting_summary',
12 | // label: 'SS Summary',
13 | // processorName: 'dandi-vis-1.spike_sorting_summary',
14 | // outputFileBaseName: 'spike_sorting_summary.nh5',
15 | // icon:
16 | // },
17 | // {
18 | // name: 'ecephys_summary',
19 | // label: 'Ecephys Summary',
20 | // processorName: 'dandi-vis-1.ecephys_summary',
21 | // outputFileBaseName: 'ecephys_summary.nh5',
22 | // icon:
23 | // }
24 | ]
25 |
--------------------------------------------------------------------------------
/src/pages/ProjectPage/FileBrowser/DropdownMenu.css:
--------------------------------------------------------------------------------
1 | .DropdownMenu li.enabled:hover {
2 | /* background-color:rgb(212, 210, 210); */
3 | color: rgb(131, 129, 129);
4 | }
5 |
--------------------------------------------------------------------------------
/src/pages/ProjectPage/FileBrowser/DropdownMenu.tsx:
--------------------------------------------------------------------------------
1 | import { Menu } from '@mui/icons-material';
2 | import React, { useState } from 'react';
3 | import { SmallIconButton } from "@fi-sci/misc";
4 | import './DropdownMenu.css';
5 |
6 | type Option = {
7 | label: string;
8 | onClick?: () => void;
9 | };
10 |
11 | type MinimalDropdownProps = {
12 | options: Option[];
13 | };
14 |
15 | const DropdownMenu: React.FC = ({ options }) => {
16 | const [isOpen, setIsOpen] = useState(false)
17 |
18 | return (
19 |
20 |
} onClick={() => setIsOpen(!isOpen)} />
21 | {isOpen && (
22 |
34 | {options.map(option => (
35 | {
39 | option.onClick?.();
40 | setIsOpen(false);
41 | }}
42 | style={{
43 | padding: '5px 10px',
44 | cursor: 'pointer',
45 | textAlign: 'left',
46 | color: option.onClick ? '#000' : '#999'
47 | }}
48 | >
49 | {option.label}
50 |
51 | ))}
52 |
53 | )}
54 |
55 | );
56 | };
57 |
58 | export default DropdownMenu;
59 |
--------------------------------------------------------------------------------
/src/pages/ProjectPage/FileBrowser/file-browser-table.css:
--------------------------------------------------------------------------------
1 | .file-browser-table {
2 | width: 100%;
3 | border-collapse: collapse;
4 | font-size: 0.9em;
5 | font-family: sans-serif;
6 | text-align: left;
7 | margin: 0px 0;
8 | }
9 |
10 | .file-browser-table thead tr {
11 | background-color: #aaa;
12 | color: #ffffff;
13 | text-align: left;
14 | }
15 |
16 | .file-browser-table th,
17 | .file-browser-table td {
18 | padding: 5px 2px;
19 | }
20 |
21 | .file-browser-table tbody tr {
22 | border-bottom: thin solid #dddddd;
23 | }
24 |
25 | .file-browser-table tbody tr:nth-of-type(even) {
26 | background-color: #f3f3f3;
27 | }
28 |
29 | .file-browser-table tbody tr:last-of-type {
30 | border-bottom: 2px solid #009879;
31 | }
32 |
33 | .file-browser-table tbody tr:hover {
34 | background-color: #ededed;
35 | transition: background-color 0.3s ease;
36 | }
37 |
38 | .file-browser-context-menu {
39 | position: absolute;
40 | background-color: white;
41 | border: 1px solid #ddd;
42 | padding: 10px;
43 | box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
44 | z-index: 999;
45 | border-radius: 3px;
46 | min-width: 100px;
47 | }
48 |
49 | .file-browser-context-menu div {
50 | padding: 5px 0;
51 | cursor: pointer;
52 | }
53 |
54 | .file-browser-context-menu div:hover {
55 | background-color: #ddd;
56 | }
--------------------------------------------------------------------------------
/src/pages/ProjectPage/FileBrowser/formatByteCount.ts:
--------------------------------------------------------------------------------
1 | const formatByteCount = (a: number) => {
2 | if (a < 10000) {
3 | return `${a} bytes`
4 | }
5 | else if (a < 100 * 1024) {
6 | return `${formatNum(a / 1024)} KB`
7 | }
8 | else if (a < 100 * 1024 * 1024) {
9 | return `${formatNum(a / (1024 * 1024))} MB`
10 | }
11 | else if (a < 600 * 1024 * 1024 * 1024) {
12 | return `${formatNum(a / (1024 * 1024 * 1025))} GB`
13 | }
14 | else {
15 | return `${formatNum(a / (1024 * 1024 * 1024 * 1024))} TB`
16 | }
17 | }
18 |
19 | export const formatGiBCount = (a: number) => {
20 | return formatByteCount(a * 1000 * 1000 * 1000)
21 | }
22 |
23 | const formatNum = (a: number) => {
24 | const b = a.toFixed(2)
25 | if (Number(b) - Math.floor(Number(b)) === 0) {
26 | return a.toFixed(0)
27 | }
28 | else return b
29 | }
30 |
31 | export default formatByteCount
--------------------------------------------------------------------------------
/src/pages/ProjectPage/FileView/ElectricalSeriesSection/ElectricalSeriesSection.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, useCallback, useState } from "react"
2 | import { useElectricalSeriesPaths, useNwbFile } from "../NwbFileView"
3 | import { Hyperlink } from "@fi-sci/misc";
4 | import ModalWindow from "@fi-sci/modal-window";
5 | import { useModalWindow } from "@fi-sci/modal-window"
6 | import { useProject } from "../../ProjectPageContext"
7 | import LoadElectricalSeriesScriptWindow from "./LoadElectricalSeriesScriptWindow"
8 |
9 | type ElectricalSeriesSectionProps = {
10 | fileName: string
11 | nwbUrl?: string
12 | }
13 |
14 | const ElectricalSeriesSection: FunctionComponent = ({fileName, nwbUrl}) => {
15 | const nwbFile = useNwbFile(nwbUrl)
16 | const electricalSeriesPaths = useElectricalSeriesPaths(nwbFile)
17 | const [selectedElectricalSeriesPath, setSelectedElectricalSeriesPath] = useState('')
18 | const {visible: loadInScriptVisible, handleOpen: openLoadInScriptWindow, handleClose: closeLoadInScriptWindow} = useModalWindow()
19 | const {project} = useProject()
20 | const loadInScript = useCallback((path: string) => {
21 | setSelectedElectricalSeriesPath(path)
22 | openLoadInScriptWindow()
23 | }, [openLoadInScriptWindow])
24 | if ((!electricalSeriesPaths) || (electricalSeriesPaths.length === 0)) return (
25 | <>>
26 | )
27 | return (
28 |
29 |
30 | Electrical series: {fileName}
31 |
32 |
33 | {
34 | electricalSeriesPaths.map((path, ii) => (
35 |
36 | {path} - loadInScript(path)}>load in script
37 |
38 | ))
39 | }
40 |
41 |
45 | {selectedElectricalSeriesPath && project && fileName && (
46 |
52 | )}
53 |
54 |
55 | )
56 | }
57 |
58 | export default ElectricalSeriesSection
--------------------------------------------------------------------------------
/src/pages/ProjectPage/FileView/ElectricalSeriesSection/LoadElectricalSeriesScriptWindow.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, useCallback, useEffect, useMemo, useState } from "react"
2 | import { DendroProject } from "../../../../types/dendro-types"
3 | import Markdown from "../../../../Markdown/Markdown"
4 | import nunjucks from "nunjucks"
5 |
6 | type LoadElectricalSeriesScriptWindowProps = {
7 | onClose: () => void
8 | project: DendroProject
9 | fileName: string
10 | electricalSeriesPath: string
11 | }
12 |
13 | export const useScript = (url: string) => {
14 | const [script, setScript] = useState(`# Loading script from ${url}...`)
15 | useEffect(() => {
16 | let canceled = false
17 | ;(async () => {
18 | const resp = await fetch(url)
19 | const text = await resp.text()
20 | if (canceled) return
21 | setScript(text)
22 | })()
23 | return () => {canceled = true}
24 | }, [url])
25 | return script
26 | }
27 |
28 | const LoadElectricalSeriesScriptWindow: FunctionComponent = ({project, fileName, electricalSeriesPath}) => {
29 | const [copied, setCopied] = useState(false)
30 | const script = useScript('/scripts/load_electrical_series.py')
31 | const processedScript = useMemo(() => {
32 | return nunjucks.renderString(script, {project, fileName, electricalSeriesPath})
33 | }, [script, project, fileName, electricalSeriesPath])
34 | const markdownSource = `
35 | Use this script to load this electrical series into a SpikeInterface recording object. If this is an embargoed dandiset, then be sure to set the DANDI_API_KEY environment variable prior to running the script.
36 |
37 | You first need install dendro and spikeinterface via pip: \`pip install --upgrade dendro spikeinterface\`
38 |
39 | ${copied ? '*Copied to clipboard*' : '[Copy](#copy)'}
40 |
41 | \`\`\`python
42 | ${processedScript}
43 | \`\`\`
44 | `
45 | const processedMarkdownSource = useMemo(() => {
46 | return nunjucks.renderString(markdownSource, {project, fileName, electricalSeriesPath})
47 | }, [markdownSource, project, fileName, electricalSeriesPath])
48 | const handleLinkClick = useCallback((href: string) => {
49 | if (href === '#copy') {
50 | navigator.clipboard.writeText(processedScript)
51 | setCopied(true)
52 | }
53 | }, [processedScript])
54 | return (
55 |
56 | )
57 | }
58 |
59 | export default LoadElectricalSeriesScriptWindow
--------------------------------------------------------------------------------
/src/pages/ProjectPage/FileView/FigurlFileView.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, useEffect, useMemo, useState } from "react"
2 | import { useProject } from "../ProjectPageContext"
3 |
4 | type FigurlFileViewProps = {
5 | fileName: string
6 | width: number
7 | height: number
8 | }
9 |
10 | const FigurlFileView: FunctionComponent = ({fileName, width, height}) => {
11 | if (!fileName.endsWith('.figurl')) {
12 | throw Error('Unexpected file extension: ' + fileName)
13 | }
14 | const {files} = useProject()
15 | const file = useMemo(() => {
16 | if (!files) return undefined
17 | return files.find(f => (f.fileName === fileName))
18 | }, [files, fileName])
19 |
20 | const [url, setUrl] = useState(undefined)
21 | useEffect(() => {
22 | if (!file) return
23 | if (!file.content.startsWith('url:')) {
24 | console.warn('Unexpected file content: ' + file.content)
25 | return
26 | }
27 | let canceled = false
28 | ; (async () => {
29 | const u = await fetchTextFile(file.content.slice('url:'.length))
30 | if (canceled) return
31 | setUrl(u)
32 | })()
33 | return () => {canceled = true}
34 | }, [file])
35 |
36 | if (!files) return Loading...
37 | if (!file) return File not found: {fileName}
38 | if (!url) return Loading file content...
39 |
40 | return (
41 |
44 | )
45 | }
46 |
47 | const fetchTextFile = async (url: string) => {
48 | const response = await fetch(url)
49 | if (!response.ok) {
50 | throw Error(`Unexpected response for ${url}: ${response.status}`)
51 | }
52 | return await response.text()
53 | }
54 |
55 | export default FigurlFileView
--------------------------------------------------------------------------------
/src/pages/ProjectPage/FileView/OtherFileView.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from "react";
2 | import FileViewTable from "./FileViewTable";
3 |
4 |
5 | type Props = {
6 | fileName: string
7 | width: number
8 | height: number
9 | }
10 |
11 | const OtherFileView: FunctionComponent = ({fileName, width, height}) => {
12 | return (
13 |
14 |
15 |
16 |
17 |
...
18 |
19 |
20 | )
21 | }
22 |
23 | export default OtherFileView
--------------------------------------------------------------------------------
/src/pages/ProjectPage/FileView/SpikeSortingOutputSection/LoadSpikeSortingInScriptWindow.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, useCallback, useEffect, useMemo, useState } from "react"
2 | import { DendroProject } from "../../../../types/dendro-types"
3 | import Markdown from "../../../../Markdown/Markdown"
4 | import nunjucks from "nunjucks"
5 | import { useScript } from "../ElectricalSeriesSection/LoadElectricalSeriesScriptWindow"
6 |
7 | type LoadSpikeSortingInScriptWindowProps = {
8 | onClose: () => void
9 | project: DendroProject
10 | fileName: string
11 | }
12 |
13 | const LoadSpikeSortingInScriptWindow: FunctionComponent = ({project, fileName}) => {
14 | const [copied, setCopied] = useState(false)
15 | const script = useScript('/scripts/load_spike_sorting.py')
16 | const processedScript = useMemo(() => {
17 | return nunjucks.renderString(script, {project, fileName})
18 | }, [script, project, fileName])
19 | const markdownSource = `
20 | Use this script to load this spike sorting into a SpikeInterface sorting object. If this is an embargoed dandiset, then be sure to set the DANDI_API_KEY environment variable prior to running the script.
21 |
22 | You first need install dendro and spikeinterface via pip: \`pip install --upgrade dendro spikeinterface\`
23 |
24 | ${copied ? '*Copied to clipboard*' : '[Copy](#copy)'}
25 |
26 | \`\`\`python
27 | ${processedScript}
28 | \`\`\`
29 | `
30 | const processedMarkdownSource = useMemo(() => {
31 | return nunjucks.renderString(markdownSource, {project, fileName})
32 | }, [markdownSource, project, fileName])
33 | const handleLinkClick = useCallback((href: string) => {
34 | if (href === '#copy') {
35 | navigator.clipboard.writeText(processedScript)
36 | setCopied(true)
37 | }
38 | }, [processedScript])
39 | return (
40 |
41 | )
42 | }
43 |
44 | export default LoadSpikeSortingInScriptWindow
--------------------------------------------------------------------------------
/src/pages/ProjectPage/JobView/OutputsTable.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, useCallback } from "react"
2 | import { Hyperlink } from "@fi-sci/misc";
3 | import { DendroJob } from "../../../types/dendro-types"
4 | import useRoute from "../../../useRoute"
5 | import { useProject } from "../ProjectPageContext"
6 |
7 | type OutputsTableProps = {
8 | job: DendroJob
9 | }
10 |
11 | const OutputsTable: FunctionComponent = ({ job }) => {
12 | const {openTab} = useProject()
13 | const {setRoute} = useRoute()
14 | const handleOpenFile = useCallback((fileName: string) => {
15 | openTab(`file:${fileName}`)
16 | setRoute({
17 | page: 'project',
18 | projectId: job.projectId,
19 | tab: `project-files`
20 | })
21 | }, [openTab, setRoute, job.projectId])
22 | return (
23 |
24 |
25 | {
26 | job.processorSpec.outputs.map((output, ii) => {
27 | const x = job.outputFiles.find(x => (x.name === output.name))
28 | return (
29 |
30 | {output.name}
31 |
32 | {
34 | x && handleOpenFile(x?.fileName)
35 | }}
36 | >
37 | {x?.fileName || 'unknown'}
38 |
39 |
40 | {output.description}
41 |
42 | )
43 | })
44 | }
45 |
46 |
47 | )
48 | }
49 |
50 | export default OutputsTable
--------------------------------------------------------------------------------
/src/pages/ProjectPage/JobsWindow/JobsTableMenuBar.tsx:
--------------------------------------------------------------------------------
1 | import { SmallIconButton } from "@fi-sci/misc";
2 | import { Check, Delete, Refresh } from "@mui/icons-material";
3 | import { FunctionComponent, useCallback, useState } from "react";
4 | import { confirm } from "../../../confirm_prompt_alert";
5 | import { useProject } from "../ProjectPageContext";
6 |
7 | type JobsTableMenuBarProps = {
8 | width: number
9 | height: number
10 | selectedJobIds: string[]
11 | onResetSelection: () => void
12 | createJobEnabled?: boolean
13 | createJobTitle?: string
14 | onApproveAll?: () => void
15 | }
16 |
17 | const JobsTableMenuBar: FunctionComponent = ({width, height, selectedJobIds, onResetSelection, createJobEnabled, createJobTitle, onApproveAll: onApproveAll}) => {
18 | const {deleteJob, refreshJobs, refreshFiles, projectRole} = useProject()
19 | const [operating, setOperating] = useState(false)
20 | const handleDelete = useCallback(async () => {
21 | if (!['admin', 'editor'].includes(projectRole || '')) {
22 | alert('You are not authorized to delete jobs in this project.')
23 | return
24 | }
25 | const okay = await confirm(`Are you sure you want to delete these ${selectedJobIds.length} jobs?`)
26 | if (!okay) return
27 | try {
28 | setOperating(true)
29 | for (const jobId of selectedJobIds) {
30 | await deleteJob(jobId)
31 | }
32 | }
33 | finally {
34 | setOperating(false)
35 | refreshJobs()
36 | refreshFiles()
37 | onResetSelection()
38 | }
39 | }, [selectedJobIds, deleteJob, refreshJobs, refreshFiles, onResetSelection, projectRole])
40 |
41 | return (
42 |
43 | }
45 | disabled={operating}
46 | title='Refresh'
47 | onClick={refreshJobs}
48 | />
49 | }
51 | disabled={(selectedJobIds.length === 0) || operating}
52 | title={selectedJobIds.length > 0 ? `Delete these ${selectedJobIds.length} jobs` : ''}
53 | onClick={handleDelete}
54 | />
55 | {
56 | onApproveAll && (
57 | }
59 | disabled={operating}
60 | title='Approve all jobs'
61 | label="Approve all jobs"
62 | onClick={onApproveAll}
63 | />
64 | )
65 | }
66 |
67 | )
68 | }
69 |
70 | export default JobsTableMenuBar
--------------------------------------------------------------------------------
/src/pages/ProjectPage/JobsWindow/JobsWindow.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, useMemo } from "react";
2 | import { useProject } from "../ProjectPageContext";
3 | import JobsTable from "./JobsTable";
4 | import { useGithubAuth } from "../../../GithubAuth/useGithubAuth";
5 |
6 | type Props = {
7 | width: number,
8 | height: number,
9 | fileName: string
10 | createJobEnabled?: boolean
11 | createJobTitle?: string
12 | }
13 |
14 | const JobsWindow: FunctionComponent = ({ width, height, fileName, createJobEnabled, createJobTitle }) => {
15 | const {jobs, openTab, computeResource} = useProject()
16 |
17 | const filteredJobs = useMemo(() => {
18 | if (!jobs) return undefined
19 | if (fileName) {
20 | return jobs.filter(jj => (
21 | jj.inputFiles.map(x => (x.fileName)).includes(fileName) ||
22 | jj.outputFiles.map(x => (x.fileName)).includes(fileName)
23 | ))
24 | }
25 | else return jobs
26 | }, [jobs, fileName])
27 |
28 | // const iconFontSize = 20
29 |
30 | const auth = useGithubAuth()
31 |
32 | const userIsComputeResourceOwner = useMemo(() => {
33 | if (!computeResource) return false
34 | if (!auth.userId) return false
35 | return computeResource.ownerId === auth.userId
36 | }, [computeResource, auth.userId])
37 |
38 | return (
39 | openTab(`job:${jobId}`)}
45 | createJobEnabled={createJobEnabled}
46 | createJobTitle={createJobTitle}
47 | userCanApproveJobs={userIsComputeResourceOwner}
48 | />
49 | // <>
50 | //
51 | // {fileName.endsWith('.py') && (
52 | // }
54 | // onClick={handleCreateScriptJob}
55 | // disabled={!canCreateJob}
56 | // title={createJobTitle}
57 | // label="Run"
58 | // fontSize={iconFontSize}
59 | // />
60 | // )}
61 | //
62 | // }
64 | // onClick={refreshJobs}
65 | // title="Refresh jobs"
66 | // label="Refresh"
67 | // fontSize={iconFontSize}
68 | // />
69 | //
70 | // >
71 | )
72 | }
73 |
74 | export default JobsWindow
--------------------------------------------------------------------------------
/src/pages/ProjectPage/LoadNwbInPythonWindow/LoadNwbInPythonWindow.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, useMemo } from "react"
2 | import Markdown from "../../../Markdown/Markdown"
3 | import { DendroProject } from "../../../types/dendro-types"
4 |
5 | type LoadNwbInPythonWindowProps = {
6 | onClose: () => void
7 | project: DendroProject
8 | fileName: string
9 | }
10 |
11 | const getMdSource = (project: DendroProject, fileName: string) => {
12 | const source = `
13 | \`\`\`python
14 | import h5py
15 | import pynwb
16 | import dendro.client as prc
17 | import remfile
18 |
19 |
20 | # Load project ${project.name}
21 | project = prc.load_project('${project.projectId}')
22 |
23 | # Lazy load ${fileName}
24 | nwb_file = project.get_file('${fileName}')
25 | nwb_remf = remfile.File(nwb_file)
26 | io = pynwb.NWBHDF5IO(file=h5py.File(nwb_remf, 'r'), mode='r')
27 | nwb = io.read()
28 |
29 | # Explore the NWB file
30 | print(nwb)
31 | \`\`\`
32 | `
33 | return source
34 | }
35 |
36 | const LoadNwbInPythonWindow: FunctionComponent = ({project, fileName}) => {
37 | const source = useMemo(() => (getMdSource(project, fileName)), [project, fileName])
38 | return (
39 |
40 | )
41 | }
42 |
43 | export default LoadNwbInPythonWindow
--------------------------------------------------------------------------------
/src/pages/ProjectPage/Processor.css:
--------------------------------------------------------------------------------
1 | .Processor {
2 | }
3 |
4 | .Processor:hover {
5 | background: #eee;
6 | }
7 |
8 | .ProcessorImage {
9 | }
10 |
11 | .ProcessorTitle {
12 | font-size: 20px;
13 | font-weight: bold;
14 | }
15 |
16 | .ProcessorDescription {
17 | font-size: 16px;
18 | }
19 |
20 | .ProcessorParameters {
21 | font-size: 14px;
22 | color: #444;
23 | }
24 |
25 | .ProcessorTags {
26 | font-size: 14px;
27 | color: #444;
28 | }
--------------------------------------------------------------------------------
/src/pages/ProjectPage/ProjectAnalysis/AnalysisSourceClient.ts:
--------------------------------------------------------------------------------
1 | import ClonedRepo, { joinPaths } from "./ClonedRepo";
2 |
3 | class AnalysisSourceClient {
4 | constructor(private url: string, private path: string, private clonedRepo: ClonedRepo) {
5 | }
6 | static async create(url: string, setStatus: (status: string) => void) {
7 | const {repoUrl, branch, path} = parseAnalysisSourceUrl(url);
8 | setStatus(`Cloning repo ${repoUrl}`)
9 | const clonedRepo = new ClonedRepo({url: repoUrl, branch})
10 | await clonedRepo.initialize(setStatus)
11 | return new AnalysisSourceClient(url, path, clonedRepo);
12 | }
13 | async readDirectory(path: string) {
14 | const fullPath = joinPaths(this.path, path);
15 | const {subdirectories, files} = await this.clonedRepo.readDirectory(fullPath);
16 | return {subdirectories, files};
17 | }
18 | async readTextFile(path: string) {
19 | const fullPath = joinPaths(this.path, path);
20 | const txt = await this.clonedRepo.readTextFile(fullPath);
21 | return txt;
22 | }
23 | }
24 |
25 | export const parseAnalysisSourceUrl = (url: string) => {
26 | // for example, url = https://github.com/magland/dendro_analysis/tree/main/projects/eb87e88a
27 | if (!url.startsWith('https://github.com')) {
28 | throw new Error('Invalid url: ' + url);
29 | }
30 | const parts = url.split('/');
31 | const owner = parts[3];
32 | const repo = parts[4];
33 | const a = parts[5];
34 | if (a === 'tree') {
35 | const branch = parts[6];
36 | const path = parts.slice(7).join('/');
37 | if (!branch) throw new Error('Invalid url: ' + url);
38 | if (!path) throw new Error('Invalid url: ' + url);
39 | return {
40 | repoUrl: `https://github.com/${owner}/${repo}`,
41 | branch,
42 | path,
43 | repoName: repo
44 | };
45 | }
46 | else if (!a) {
47 | return {
48 | repoUrl: url,
49 | branch: '',
50 | path: '',
51 | repoName: repo
52 | };
53 | }
54 | else {
55 | throw new Error('Invalid url: ' + url);
56 | }
57 | }
58 |
59 | export default AnalysisSourceClient;
--------------------------------------------------------------------------------
/src/pages/ProjectPage/ProjectAnalysis/AnalysisSourceFileBrowser.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, useMemo, useReducer } from "react"
2 | import { selectedStringsReducer } from "../FileBrowser/FileBrowser2"
3 | import FileBrowserTable, { FileBrowserTableFile } from "../FileBrowser/FileBrowserTable"
4 | import AnalysisSourceClient from "./AnalysisSourceClient"
5 |
6 | type AnalysisSourceFileBrowserProps = {
7 | width: number
8 | height: number
9 | analysisSourceClient: AnalysisSourceClient
10 | onOpenFile: (path: string) => void
11 | allFiles?: AnalysisSourceFile[]
12 | }
13 |
14 | export type AnalysisSourceFile = {
15 | path: string
16 | }
17 |
18 | const AnalysisSourceFileBrowser: FunctionComponent = ({width, height, analysisSourceClient, onOpenFile, allFiles}) => {
19 | const [selectedFileNames, selectedFileNamesDispatch] = useReducer(selectedStringsReducer, new Set())
20 |
21 | const files2: FileBrowserTableFile[] | undefined = useMemo(() => {
22 | if (!allFiles) return undefined
23 | return allFiles.map(f => ({
24 | fileName: f.path,
25 | size: 0,
26 | timestampCreated: 0,
27 | content: undefined
28 | }))
29 | }, [allFiles])
30 |
31 | if (!allFiles) {
32 | return Loading files...
33 | }
34 |
35 | return (
36 |
37 |
47 |
48 | )
49 | }
50 |
51 | export default AnalysisSourceFileBrowser
--------------------------------------------------------------------------------
/src/pages/ProjectPage/ProjectAnalysis/AnalysisSourceFileView.css:
--------------------------------------------------------------------------------
1 | .ipynb-renderer-root pre {
2 | font-size: 12px!important;
3 | }
4 |
5 | .ipynb-renderer-root div.output_html {
6 | font-size: 15px!important;
7 | }
--------------------------------------------------------------------------------
/src/pages/ProjectPage/ProjectAnalysis/fs.ts:
--------------------------------------------------------------------------------
1 | import FS from "@isomorphic-git/lightning-fs"
2 | import {Buffer} from 'buffer'
3 |
4 | window.Buffer = Buffer
5 |
6 | const fs = new FS('default')
7 |
8 | export default fs
--------------------------------------------------------------------------------
/src/pages/ProjectPage/ProjectJobs.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, useMemo, useState } from "react";
2 | import { Splitter } from "@fi-sci/splitter";
3 | import JobsTable from "./JobsWindow/JobsTable";
4 | import JobView from "./JobView/JobView";
5 | import { useProject } from "./ProjectPageContext";
6 | import { useGithubAuth } from "../../GithubAuth/useGithubAuth";
7 |
8 | const ProjectJobs: FunctionComponent<{width: number, height: number}> = ({width, height}) => {
9 | const {jobs, computeResource} = useProject()
10 | const [selectedJobId, setSelectedJobId] = useState(undefined)
11 |
12 | const auth = useGithubAuth()
13 |
14 | const userIsComputeResourceOwner = useMemo(() => {
15 | if (!computeResource) return false
16 | if (!auth.userId) return false
17 | return computeResource.ownerId === auth.userId
18 | }, [computeResource, auth.userId])
19 |
20 | return (
21 | j.jobId).includes(selectedJobId))}
27 | >
28 | setSelectedJobId(jobId)}
34 | userCanApproveJobs={userIsComputeResourceOwner}
35 | />
36 |
41 |
42 | )
43 | }
44 |
45 | export default ProjectJobs
--------------------------------------------------------------------------------
/src/pages/ProjectPage/ProjectScripts.tsx:
--------------------------------------------------------------------------------
1 | import { Splitter } from "@fi-sci/splitter";
2 | import { FunctionComponent, useCallback, useMemo, useState } from "react";
3 | import { useProject } from "./ProjectPageContext";
4 | import ScriptsTable from "./scripts/ScriptsTable";
5 | import ScriptView from "./scripts/ScriptView";
6 | import { setScriptContent } from "../../dbInterface/dbInterface";
7 | import { useGithubAuth } from "../../GithubAuth/useGithubAuth";
8 |
9 | const ProjectScripts: FunctionComponent<{width: number, height: number}> = ({width, height}) => {
10 | const {scripts, projectRole, refreshScripts} = useProject()
11 | const [selectedScriptId, setSelectedScriptId] = useState(undefined)
12 |
13 | const createScriptEnabled = projectRole === 'admin' || projectRole === 'editor'
14 |
15 | const selectedScript = useMemo(() => {
16 | if (!selectedScriptId) return undefined
17 | return scripts?.find(s => s.scriptId === selectedScriptId)
18 | }, [selectedScriptId, scripts])
19 |
20 | const auth = useGithubAuth()
21 |
22 | const handleSetScriptContent = useCallback(async (content: string) => {
23 | if (!selectedScriptId) return
24 | await setScriptContent(selectedScriptId, content, auth)
25 | refreshScripts()
26 | }, [selectedScriptId, auth, refreshScripts])
27 |
28 | return (
29 | s.scriptId).includes(selectedScriptId))}
35 | >
36 | {scripts ? setSelectedScriptId(scriptId)}
41 | createScriptEnabled={createScriptEnabled}
42 | createScriptTitle={createScriptEnabled ? "Create a new script" : ""}
43 | /> :
}
44 |
50 |
51 | )
52 | }
53 |
54 | export default ProjectScripts
--------------------------------------------------------------------------------
/src/pages/ProjectPage/ResourceUtilizationView/LogPlot.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from "react";
2 | import Plot from 'react-plotly.js';
3 |
4 |
5 | type LogPlotProps = {
6 | series: {
7 | label: string,
8 | data: {x: number, y: number}[]
9 | color: string
10 | }[],
11 | referenceTime: number
12 | yAxisLabel?: string
13 | }
14 |
15 | const LogPlot: FunctionComponent = ({series, referenceTime, yAxisLabel}) => {
16 | const height = 220
17 | const data = series.map(s => ({
18 | x: s.data.map(d => d.x - referenceTime),
19 | y: s.data.map(d => d.y),
20 | name: s.label,
21 | mode: 'lines',
22 | line: {
23 | color: s.color
24 | }
25 | }))
26 | const xAxisLabel = 'Time (s)'
27 | return (
28 |
49 | )
50 | }
51 |
52 | export default LogPlot
--------------------------------------------------------------------------------
/src/pages/ProjectPage/RunBatchSpikeSortingWindow/ElectrodeGeometryView.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, useEffect, useState } from "react"
2 | import { RemoteH5File } from "../../../RemoteH5File/RemoteH5File"
3 | import {ElectrodeGeometryWidget, ElectrodeLocation} from "@fi-sci/electrode-geometry"
4 |
5 | type ElectrodeGeometryViewProps = {
6 | width: number
7 | height: number
8 | nwbFile: RemoteH5File
9 | }
10 |
11 | const ElectrodeGeometryView: FunctionComponent = ({width, height, nwbFile}) => {
12 | const [electrodeLocations, setElectrodeLocations] = useState(undefined)
13 | useEffect(() => {
14 | (async () => {
15 | setElectrodeLocations(undefined)
16 | const grp = await nwbFile.getGroup('/general/extracellular_ephys/electrodes')
17 | if (!grp) return
18 | if (!grp.datasets) return
19 | let xDatasetPath = ''
20 | let yDatasetPath = ''
21 | if (grp.datasets.find(ds => (ds.name === 'rel_x'))) {
22 | xDatasetPath = '/general/extracellular_ephys/electrodes/rel_x'
23 | yDatasetPath = '/general/extracellular_ephys/electrodes/rel_y'
24 | }
25 | else if (grp.datasets.find(ds => (ds.name === 'x'))) {
26 | xDatasetPath = '/general/extracellular_ephys/electrodes/x'
27 | yDatasetPath = '/general/extracellular_ephys/electrodes/y'
28 | }
29 | else {
30 | return
31 | }
32 | if ((!xDatasetPath) || (!yDatasetPath)) return
33 | const x = await nwbFile.getDatasetData(xDatasetPath, {})
34 | const y = await nwbFile.getDatasetData(yDatasetPath, {})
35 | const locations: ElectrodeLocation[] = []
36 | for (let i = 0; i < x.length; i++) {
37 | locations.push({x: x[i], y: y[i]})
38 | }
39 | setElectrodeLocations(locations)
40 | })()
41 | }, [nwbFile])
42 | if (!electrodeLocations) return
43 | return (
44 |
49 | )
50 | }
51 |
52 | export default ElectrodeGeometryView
--------------------------------------------------------------------------------
/src/pages/ProjectPage/RunBatchSpikeSortingWindow/SelectProcessorComponent.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from "react"
2 | import { Hyperlink } from "@fi-sci/misc";
3 | import { ComputeResourceSpecApp, ComputeResourceSpecProcessor, DendroComputeResourceApp } from "../../../types/dendro-types"
4 |
5 | type SelectProcessorComponentProps = {
6 | processors: {appSpec: ComputeResourceSpecApp, app?: DendroComputeResourceApp, processor: ComputeResourceSpecProcessor}[]
7 | onSelected: (processorName: string) => void
8 | }
9 |
10 | const SelectProcessorComponent: FunctionComponent = ({processors, onSelected}) => {
11 | return (
12 |
13 |
Select a spike sorter
14 |
15 |
16 |
17 | Name
18 | App
19 |
20 |
21 |
22 | {
23 | processors.map((processor, i) => (
24 |
25 |
26 | {onSelected(processor.processor.name)}}>
27 | {processor.processor.name}
28 |
29 |
30 |
31 | {processor.appSpec.name}
32 |
33 |
34 | ))
35 | }
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | export default SelectProcessorComponent
--------------------------------------------------------------------------------
/src/pages/ProjectPage/RunBatchSpikeSortingWindow/getDefaultRequiredResources.ts:
--------------------------------------------------------------------------------
1 | import { ComputeResourceSpecProcessor, DendroJobRequiredResources } from "../../../types/dendro-types"
2 |
3 |
4 | const getDefaultRequiredResources = (processor: ComputeResourceSpecProcessor | undefined): DendroJobRequiredResources | undefined => {
5 | if (!processor) return undefined
6 | const tags = processor.tags.map(t => t.tag)
7 | if ((tags.includes('kilosort2_5') || tags.includes('kilosort3'))) {
8 | return {
9 | numCpus: 4,
10 | numGpus: 1,
11 | memoryGb: 16,
12 | timeSec: 3600 * 3 // todo: determine this based on the size of the recording!
13 | }
14 | }
15 | else if (tags.includes('mountainsort5')) {
16 | return {
17 | numCpus: 8,
18 | numGpus: 0,
19 | memoryGb: 16,
20 | timeSec: 3600 * 3 // todo: determine this based on the size of the recording!
21 | }
22 | }
23 | else {
24 | return {
25 | numCpus: 8,
26 | numGpus: 0,
27 | memoryGb: 16,
28 | timeSec: 3600 * 3 // todo: determine this based on the size of the recording!
29 | }
30 | }
31 | }
32 |
33 | export default getDefaultRequiredResources
--------------------------------------------------------------------------------
/src/pages/ProjectPage/UploadSmalFileWindow/UploadSmallFileWindow.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, useCallback } from "react"
2 | import { createFileAndInitiateUpload } from "../../../dbInterface/dbInterface"
3 | import { useProject } from "../ProjectPageContext"
4 | import { useGithubAuth } from "../../../GithubAuth/useGithubAuth"
5 |
6 | type UploadSmallFileWindowProps = {
7 | onClose: () => void
8 | }
9 |
10 | const UploadSmallFileWindow: FunctionComponent = ({ onClose }) => {
11 | const {projectId, refreshFiles} = useProject()
12 | const auth = useGithubAuth()
13 | const handleSelectFile = useCallback(() => {
14 | if (!auth) return
15 | const fileInput = document.createElement("input")
16 | fileInput.type = "file"
17 | fileInput.addEventListener("change", () => {
18 | const file = fileInput.files?.[0]
19 | if (!file) return
20 | // get the binary content of the file
21 | const reader = new FileReader()
22 | reader.readAsBinaryString(file)
23 | reader.addEventListener("load", () => {
24 | const content = reader.result
25 | if (!content) return
26 | ; (async () => {
27 | const {uploadUrl} = await createFileAndInitiateUpload(
28 | projectId,
29 | `uploads/${file.name}`,
30 | contentSize(content),
31 | auth
32 | )
33 | const uploadRequest = new XMLHttpRequest()
34 | uploadRequest.open("PUT", uploadUrl)
35 | uploadRequest.setRequestHeader("Content-Type", "application/octet-stream")
36 | uploadRequest.send(content)
37 | uploadRequest.addEventListener("load", () => {
38 | refreshFiles()
39 | onClose()
40 | })
41 | uploadRequest.addEventListener("error", (e) => {
42 | console.error(e)
43 | alert("Upload failed")
44 | }, {once: true})
45 | })()
46 | })
47 | })
48 | fileInput.click()
49 | }, [auth, projectId, onClose, refreshFiles])
50 | if (!auth) {
51 | return Please login first.
52 | }
53 | return (
54 |
55 |
56 | Select a small file from from your computer
57 |
58 |
59 | )
60 | }
61 |
62 | const contentSize = (content: string | ArrayBuffer) => {
63 | if (typeof content === "string") {
64 | return content.length
65 | }
66 | return content.byteLength
67 | }
68 |
69 | export default UploadSmallFileWindow
70 |
--------------------------------------------------------------------------------
/src/pages/ProjectPage/openFilesInNeurosift.ts:
--------------------------------------------------------------------------------
1 | import { url } from "inspector";
2 | import { DendroFile } from "../../types/dendro-types";
3 |
4 | const openFilesInNeurosift = async (files: DendroFile[], dendroProjectId: string) => {
5 | const urls = files.map((file) => urlFromFileContent(file.content));
6 | let urlQuery = urls.map((url) => `url=${url}`).join('&');
7 | // const fileNameQuery = files.map((file) => `fileName=${file.fileName}`).join('&');
8 | // const neurosiftUrl = `https://flatironinstitute.github.io/neurosift/?p=/nwb&${urlQuery}&dendroProjectId=${dendroProjectId}&${fileNameQuery}`;
9 | const dandisetId = files[0]?.metadata.dandisetId;
10 | const dandisetVersion = files[0]?.metadata.dandisetVersion;
11 | const dandiAssetId = files[0]?.metadata.dandiAssetId;
12 | if (dandisetId) urlQuery += `&dandisetId=${dandisetId}`;
13 | if (dandisetVersion) urlQuery += `&dandisetVersion=${dandisetVersion}`;
14 | if (dandiAssetId) urlQuery += `&dandiAssetId=${dandiAssetId}`;
15 | const neurosiftUrl = `https://flatironinstitute.github.io/neurosift/?p=/nwb&${urlQuery}`;
16 | window.open(neurosiftUrl, '_blank');
17 | }
18 |
19 | const urlFromFileContent = (content: string) => {
20 | if (!content.startsWith('url:')) {
21 | throw new Error('Invalid file content: ' + content);
22 | }
23 | return content.substring('url:'.length);
24 | }
25 |
26 | export default openFilesInNeurosift;
--------------------------------------------------------------------------------
/src/pages/ProjectPage/scripts/CodeEditor.tsx:
--------------------------------------------------------------------------------
1 | import { Editor } from "@monaco-editor/react"
2 | import { editor as editor0 } from 'monaco-editor';
3 | import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
4 | import { FunctionComponent, useCallback, useEffect, useState } from "react"
5 |
6 | type Monaco = typeof monaco
7 |
8 | type CodeEditorProps = {
9 | width: number
10 | height: number
11 | content: string
12 | readOnly: boolean
13 | onContentChanged: (content: string) => void
14 | onSave: () => void
15 | }
16 |
17 | const CodeEditor: FunctionComponent = ({width, height, content, readOnly, onContentChanged, onSave}) => {
18 | const handleChange = useCallback((value: string | undefined) => {
19 | if (value === undefined) return
20 | onContentChanged(value)
21 | }, [onContentChanged])
22 | const wordWrap = false
23 |
24 | const [editor, setEditor] = useState(undefined)
25 | useEffect(() => {
26 | if (!editor) return
27 | if (editor.getValue() === content) return
28 | editor.setValue(content || '')
29 | }, [editor, content])
30 | const handleEditorDidMount = useCallback((editor: editor0.IStandaloneCodeEditor, monaco: Monaco) => {
31 | (async () => {
32 | // fancy stuff would go here
33 | setEditor(editor)
34 | })()
35 | }, [])
36 | // Can't do this in the usual way with monaco editor:
37 | // See: https://github.com/microsoft/monaco-editor/issues/2947
38 | const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
39 | if ((e.ctrlKey || e.metaKey) && e.key === 's') {
40 | e.preventDefault()
41 | if (readOnly) return
42 | if (!readOnly) {
43 | onSave()
44 | }
45 | }
46 | }, [onSave, readOnly])
47 | return (
48 |
49 |
62 |
63 | )
64 | }
65 |
66 | export default CodeEditor
--------------------------------------------------------------------------------
/src/pages/ProjectPage/scripts/RunScript/RunScriptWorkerTypes.ts:
--------------------------------------------------------------------------------
1 | export type RunScriptAddJobInputFile = {
2 | name: string
3 | fileName: string
4 | isFolder?: boolean
5 | }
6 |
7 | export type RunScriptAddJobOutputFile = {
8 | name: string
9 | fileName: string
10 | isFolder?: boolean
11 | skipCloudUpload?: boolean
12 | }
13 |
14 | export type RunScriptAddJobParameter = {
15 | name: string
16 | value: any
17 | }
18 |
19 | export type RunScriptAddJobRequiredResources = {
20 | numCpus: number
21 | numGpus: number
22 | memoryGb: number
23 | timeSec: number
24 | }
25 |
26 | export type RunScriptAddJob = {
27 | processorName: string
28 | inputFiles: RunScriptAddJobInputFile[]
29 | outputFiles: RunScriptAddJobOutputFile[]
30 | inputParameters: RunScriptAddJobParameter[]
31 | requiredResources: RunScriptAddJobRequiredResources
32 | runMethod: 'local' | 'aws_batch' | 'slurm'
33 | }
34 |
35 | export type RunScriptAddedFile = {
36 | fileName: string
37 | url: string
38 | }
39 |
40 | export type RunScriptResult = {
41 | jobs: RunScriptAddJob[]
42 | addedFiles: RunScriptAddedFile[]
43 | }
44 |
45 | export {}
--------------------------------------------------------------------------------
/src/pages/ProjectsPage/ProjectsPage.css:
--------------------------------------------------------------------------------
1 | .projects-page {
2 | /* padding: 20px; */
3 | font-family: Arial, sans-serif;
4 | max-width: 1400px;
5 | margin: auto;
6 | }
7 |
8 | .projects-page h3 {
9 | color: #333;
10 | }
11 |
12 | .projects-page p {
13 | margin-bottom: 20px;
14 | text-align: justify;
15 | }
--------------------------------------------------------------------------------
/src/pages/ProjectsPage/ProjectsPage.tsx:
--------------------------------------------------------------------------------
1 | import { Hyperlink, SmallIconButton } from "@fi-sci/misc";
2 | import { Add, ImportExport } from "@mui/icons-material";
3 | import { FunctionComponent, useCallback } from "react";
4 | import useRoute from "../../useRoute";
5 | import './ProjectsPage.css';
6 | import ProjectsTable from "./ProjectsTable";
7 | import { createProject } from "../../dbInterface/dbInterface";
8 | import { useGithubAuth } from "../../GithubAuth/useGithubAuth";
9 |
10 | type Props = {
11 | width: number
12 | height: number
13 | }
14 |
15 | const ProjectsPage: FunctionComponent = ({width, height}) => {
16 | const {setRoute} = useRoute()
17 |
18 | const auth = useGithubAuth()
19 |
20 | const handleAdd = useCallback(async () => {
21 | if (!auth.signedIn) {
22 | alert('You must be signed in to create a project')
23 | return
24 | }
25 | const projectName = prompt('Enter a name for your project', 'untitled')
26 | if (!projectName) return
27 | const newProjectId = await createProject(projectName, auth)
28 | setRoute({page: 'project', projectId: newProjectId})
29 | }, [setRoute, auth])
30 |
31 | return (
32 |
33 |
Your projects
34 |
35 | } onClick={handleAdd} label="Create a new project" />
36 |
37 | } onClick={() => setRoute({page: 'dandisets'})} label="Import data from DANDI" />
38 |
39 |
40 |
41 |
42 | )
43 | }
44 |
45 | export default ProjectsPage
--------------------------------------------------------------------------------
/src/pages/ProjectsPage/useProjectsForUser.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useGithubAuth } from "../../GithubAuth/useGithubAuth";
3 | import { fetchAdminAllProjects, fetchProjectsForUser } from "../../dbInterface/dbInterface";
4 | import { DendroProject } from "../../types/dendro-types";
5 |
6 | const useProjectsForUser = (o: {admin?: boolean} = {}): DendroProject[] | undefined => {
7 | const [projects, setProjects] = useState(undefined)
8 | const auth = useGithubAuth()
9 | useEffect(() => {
10 | let canceled = false
11 |
12 | if (!auth) {
13 | setProjects([])
14 | return
15 | }
16 |
17 | setProjects(undefined)
18 |
19 | ; (async () => {
20 | let projects
21 | if (!o.admin) {
22 | projects = await fetchProjectsForUser(auth)
23 | }
24 | else {
25 | projects = await fetchAdminAllProjects(auth)
26 | }
27 | if (canceled) return
28 | setProjects(projects)
29 | })()
30 | return () => {
31 | canceled = true
32 | }
33 | }, [auth, o.admin])
34 | return projects
35 | }
36 |
37 | export default useProjectsForUser
--------------------------------------------------------------------------------
/src/pages/RegisterComputeResourcePage/RegisterComputeResourcePage.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, useCallback, useMemo, useState } from "react";
2 | import { registerComputeResource } from "../../dbInterface/dbInterface";
3 | import { useGithubAuth } from "../../GithubAuth/useGithubAuth";
4 | import UserIdComponent from "../../UserIdComponent";
5 | import useRoute from "../../useRoute";
6 |
7 | type Props = {
8 | // none
9 | }
10 |
11 | const RegisterComputeResourcePage: FunctionComponent = () => {
12 | const {route, setRoute} = useRoute()
13 | const [name, setName] = useState('')
14 |
15 | if (route.page !== 'register-compute-resource') throw Error('Unexpected')
16 |
17 | const {computeResourceId, resourceCode} = route
18 |
19 | const auth = useGithubAuth()
20 |
21 | const handleRegister = useCallback(async () => {
22 | if (!auth) return
23 | try {
24 | await registerComputeResource(computeResourceId, resourceCode, name, auth)
25 | }
26 | catch(e) {
27 | alert(`Error registering compute resource: ${e}`)
28 | return
29 | }
30 | setRoute({page: 'compute-resources'})
31 | }, [computeResourceId, resourceCode, name, auth, setRoute])
32 |
33 | if (!auth.userId) {
34 | return To register this compute resource, you must first log in. Click the "Log in" button in the upper right corner.
35 | }
36 |
37 | return (
38 |
39 |
You are registering compute resource {abbreviate(computeResourceId, 12)} to user
40 |
41 | Choose a name for this resource: setName(e.target.value)} />
42 |
43 |
44 |
45 | Register
46 |
47 |
48 | )
49 | }
50 |
51 | const abbreviate = (s: string, maxLength: number) => {
52 | if (s.length <= maxLength) return s
53 | return s.slice(0, maxLength - 3) + '...'
54 | }
55 |
56 | export default RegisterComputeResourcePage;
--------------------------------------------------------------------------------
/src/plugins/DendroFrontendPlugin.ts:
--------------------------------------------------------------------------------
1 | import { DendroJobRequiredResources } from "../types/dendro-types"
2 |
3 | export type PluginAction = {
4 | type: 'processor-that-generates-a-single-output',
5 | name: string,
6 | label: string,
7 | defaultOutputFileName: string,
8 | processorTag: string,
9 | defaultRequiredResources: DendroJobRequiredResources
10 | }
11 |
12 | export type PluginContext = {
13 | registerAction: (action: PluginAction) => void
14 | }
15 |
16 | export type DendroFrontendPlugin = {
17 | pluginName: string
18 | initialize: (context: PluginContext) => void
19 | }
--------------------------------------------------------------------------------
/src/plugins/initializePlugins.ts:
--------------------------------------------------------------------------------
1 | import { PluginAction, PluginContext } from "./DendroFrontendPlugin"
2 | import mearecPlugin from "./mearec/mearecPlugin"
3 |
4 | const initializePlugins = () => {
5 | const actions: PluginAction[] = []
6 | const pluginContext: PluginContext = {
7 | registerAction: (action) => {
8 | actions.push(action)
9 | }
10 | }
11 | mearecPlugin.initialize(pluginContext)
12 |
13 | return {
14 | actions
15 | }
16 | }
17 |
18 | export default initializePlugins
--------------------------------------------------------------------------------
/src/plugins/mearec/mearecPlugin.ts:
--------------------------------------------------------------------------------
1 | import { DendroFrontendPlugin, PluginContext } from "../DendroFrontendPlugin"
2 |
3 | const mearecPlugin: DendroFrontendPlugin = {
4 | pluginName: 'mearec',
5 | initialize: (context: PluginContext) => {
6 | context.registerAction({
7 | name: 'mearec-generate-templates',
8 | type: 'processor-that-generates-a-single-output',
9 | label: 'MEArec: generate templates',
10 | defaultOutputFileName: 'generated/mearec/default.templates.h5',
11 | processorTag: 'mearec_generate_templates',
12 | defaultRequiredResources: {
13 | numCpus: 4,
14 | numGpus: 0,
15 | memoryGb: 8,
16 | timeSec: 60 * 60
17 | }
18 | })
19 | }
20 | }
21 |
22 | export default mearecPlugin
23 |
--------------------------------------------------------------------------------
/src/pubnub/pubnub.ts:
--------------------------------------------------------------------------------
1 | import PubNub from 'pubnub'
2 |
3 | const PUBNUB_SUBSCRIBE_KEY = import.meta.env.VITE_PUBNUB_SUBSCRIBE_KEY
4 |
5 | let pnClient: PubNub | undefined = undefined
6 | if (PUBNUB_SUBSCRIBE_KEY) {
7 | pnClient = new PubNub({
8 | subscribeKey: PUBNUB_SUBSCRIBE_KEY,
9 | userId: 'browser'
10 | })
11 | }
12 | else {
13 | console.warn('PUBNUB_SUBSCRIBE_KEY not set. Not connecting to PubNub.')
14 | }
15 |
16 | export const onPubsubMessage = (callback: (message: any) => void) => {
17 | const listener = {
18 | message: (messageEvent: any) => {
19 | callback(messageEvent.message)
20 | }
21 | }
22 | if (pnClient) {
23 | pnClient.addListener(listener)
24 | }
25 | const cancel = () => {
26 | if (pnClient) {
27 | pnClient.removeListener(listener)
28 | }
29 | }
30 | return cancel
31 | }
32 |
33 | export const setPubNubListenChannel = (channel: string) => {
34 | if (pnClient) {
35 | pnClient.unsubscribeAll()
36 | pnClient.subscribe({
37 | channels: [channel]
38 | })
39 | }
40 | }
--------------------------------------------------------------------------------
/src/scientific-table.css:
--------------------------------------------------------------------------------
1 | /* Table container */
2 | .table-container {
3 | font-family: "Courier New", monospace;
4 | width: 100%;
5 | overflow-x: auto;
6 | white-space: nowrap;
7 | cursor: default;
8 | }
9 |
10 | /* Table */
11 | .scientific-table {
12 | border-collapse: collapse;
13 | width: 100%;
14 | }
15 |
16 | /* Table header */
17 | .scientific-table th {
18 | background-color: #c5af96;
19 | color: #ffffff;
20 | text-align: left;
21 | padding: 4px 8px;
22 | border: 1px solid #aaa7a7;
23 | }
24 |
25 | /* Table rows */
26 | .scientific-table tr:nth-child(even) {
27 | background-color: #f2f2f2;
28 | }
29 |
30 | .scientific-table tr:nth-child(odd) {
31 | background-color: #ffffff;
32 | }
33 |
34 | /* Table data */
35 | .scientific-table td {
36 | padding: 4px 8px;
37 | border: 1px solid #ccc;
38 | }
39 |
40 | /* Table hover */
41 | .scientific-table tbody tr:hover {
42 | background-color: #ddd;
43 | }
44 |
45 | .scientific-table tr.selected {
46 | background-color: #aad;
47 | }
48 |
49 | .scientific-table tr.selected:hover {
50 | background-color: #ccf;
51 | }
--------------------------------------------------------------------------------
/src/table1.css:
--------------------------------------------------------------------------------
1 | .table1 {
2 | border-collapse: collapse;
3 | width: 100%;
4 | text-align: left;
5 | color: #333;
6 | font-family: 'Roboto', sans-serif;
7 | }
8 |
9 | .table1 td {
10 | border: 1px solid #dfcece;
11 | padding: 3px;
12 | }
13 |
14 | .table1 tr:nth-child(even) {
15 | background-color: #f2f2f2;
16 | }
17 |
18 | .table1 tr:hover {
19 | background-color: #ddd;
20 | }
21 |
22 | .table1 td:first-child {
23 | font-weight: bold;
24 | }
25 |
26 | .table2 {
27 | border-collapse: collapse;
28 | width: 100%;
29 | text-align: left;
30 | color: #333;
31 | font-family: 'Roboto', sans-serif;
32 | }
--------------------------------------------------------------------------------
/src/timeStrings.ts:
--------------------------------------------------------------------------------
1 | // thanks https://stackoverflow.com/a/6109105/160863 and gh copilot!
2 | export function timeAgoString(timestampSeconds?: number) {
3 | if (timestampSeconds === undefined) return ''
4 | const now = Date.now()
5 | const diff = now - timestampSeconds * 1000
6 | const diffSeconds = Math.floor(diff / 1000)
7 | const diffMinutes = Math.floor(diffSeconds / 60)
8 | const diffHours = Math.floor(diffMinutes / 60)
9 | const diffDays = Math.floor(diffHours / 24)
10 | const diffWeeks = Math.floor(diffDays / 7)
11 | const diffMonths = Math.floor(diffWeeks / 4)
12 | const diffYears = Math.floor(diffMonths / 12)
13 | if (diffYears > 0) {
14 | return `${diffYears} yr${diffYears === 1 ? '' : 's'} ago`
15 | }
16 | else if (diffWeeks > 0) {
17 | return `${diffWeeks} wk${diffWeeks === 1 ? '' : 's'} ago`
18 | }
19 | else if (diffDays > 0) {
20 | return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`
21 | }
22 | else if (diffHours > 0) {
23 | return `${diffHours} hr${diffHours === 1 ? '' : 's'} ago`
24 | }
25 | else if (diffMinutes > 0) {
26 | return `${diffMinutes} min ago`
27 | }
28 | else {
29 | return `${diffSeconds} sec ago`
30 | }
31 | }
32 |
33 | export function elapsedTimeString(numSeconds?: number) {
34 | if (numSeconds === undefined) return ''
35 | numSeconds = Math.floor(numSeconds)
36 | const numMinutes = Math.floor(numSeconds / 60)
37 | const numHours = Math.floor(numMinutes / 60)
38 | const numDays = Math.floor(numHours / 24)
39 | const numWeeks = Math.floor(numDays / 7)
40 | const numMonths = Math.floor(numWeeks / 4)
41 | const numYears = Math.floor(numMonths / 12)
42 | if (numYears > 0) {
43 | return `${numYears} yr${numYears === 1 ? '' : 's'}`
44 | }
45 | else if (numWeeks > 5) {
46 | return `${numWeeks} wk${numWeeks === 1 ? '' : 's'}`
47 | }
48 | else if (numDays > 5) {
49 | return `${numDays} day${numDays === 1 ? '' : 's'}`
50 | }
51 | else if (numHours > 5) {
52 | return `${numHours} hr${numHours === 1 ? '' : 's'}`
53 | }
54 | else if (numMinutes > 5) {
55 | return `${numMinutes} min`
56 | }
57 | else {
58 | return `${numSeconds} sec`
59 | }
60 | }
--------------------------------------------------------------------------------
/src/useWindowDimensions.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | // Thanks: https://stackoverflow.com/questions/36862334/get-viewport-window-height-in-reactjs
4 | function getWindowDimensions() {
5 | const { innerWidth: width, innerHeight: height } = window;
6 | return {
7 | width,
8 | height
9 | };
10 | }
11 |
12 | const useWindowDimensions = () => {
13 | const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
14 |
15 | useEffect(() => {
16 | function handleResize() {
17 | setWindowDimensions(getWindowDimensions());
18 | }
19 |
20 | window.addEventListener('resize', handleResize);
21 | return () => window.removeEventListener('resize', handleResize);
22 | }, []);
23 |
24 | return windowDimensions;
25 | }
26 | //////////////////////////////////////////////////////////////////////////////////////////////////
27 |
28 | export default useWindowDimensions
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "allowJs": false,
7 | "skipLibCheck": true,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx"
18 | },
19 | "include": ["src"],
20 | "references": [{ "path": "./tsconfig.node.json" }]
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | // "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "Node",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | {
4 | "source": "/api/(.*)",
5 | "destination": "/api/index.py"
6 | },
7 | {
8 | "source": "/docs",
9 | "destination": "/api/index.py"
10 | },
11 | {
12 | "source": "/redoc",
13 | "destination": "/api/index.py"
14 | },
15 | {
16 | "source": "/openapi.json",
17 | "destination": "/api/index.py"
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/vercel.json.readme.md:
--------------------------------------------------------------------------------
1 | Here are some important notes for developers regarding the `vercel.json`
2 | See: https://vercel.com/docs/projects/project-configuration
3 |
4 | Note that there is a `_vercel_dev.json` and a `_vercel_prod.json`. One is required for local dev and the other is required for prod. In package.json, notice in the deploy script, the `_vercel_prod.json` gets copied onto `vercel.json` and then after the build, `_vercel_dev.json` gets copied onto the `vercel.json`. I'd like the `_vercel_dev.json` to be the one in the source repo.
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | assetsInclude: ['**/*.md']
8 | })
9 |
--------------------------------------------------------------------------------