hailer 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hailer-0.1.1/.config/hailer/README.md +43 -0
- hailer-0.1.1/.config/hailer/context/00-example.md +30 -0
- hailer-0.1.1/.config/hailer/prompts/monthly-pack.md +17 -0
- hailer-0.1.1/.config/hailer/skills/example-skill/SKILL.md +38 -0
- hailer-0.1.1/.config/hailer/skills/example-skill/reference/checks.md +15 -0
- hailer-0.1.1/.github/workflows/members-only.yml +37 -0
- hailer-0.1.1/.github/workflows/release.yml +104 -0
- hailer-0.1.1/.github/workflows/test.yml +39 -0
- hailer-0.1.1/.gitignore +37 -0
- hailer-0.1.1/PKG-INFO +508 -0
- hailer-0.1.1/PLAN.md +347 -0
- hailer-0.1.1/README.md +490 -0
- hailer-0.1.1/data/.gitkeep +0 -0
- hailer-0.1.1/data/README.md +30 -0
- hailer-0.1.1/docs/INTERFACES.md +383 -0
- hailer-0.1.1/hailer.toml +43 -0
- hailer-0.1.1/notebooks/analysis.py +92 -0
- hailer-0.1.1/pyproject.toml +47 -0
- hailer-0.1.1/scripts/__init__.py +1 -0
- hailer-0.1.1/scripts/make_sample_data.py +103 -0
- hailer-0.1.1/scripts/release.py +366 -0
- hailer-0.1.1/src/hailer/__init__.py +3 -0
- hailer-0.1.1/src/hailer/agent.py +850 -0
- hailer-0.1.1/src/hailer/browser.py +65 -0
- hailer-0.1.1/src/hailer/cli.py +1680 -0
- hailer-0.1.1/src/hailer/config.py +550 -0
- hailer-0.1.1/src/hailer/context.py +259 -0
- hailer-0.1.1/src/hailer/errors.py +63 -0
- hailer-0.1.1/src/hailer/log.py +138 -0
- hailer-0.1.1/src/hailer/marimo_client.py +739 -0
- hailer-0.1.1/src/hailer/mcp_server.py +668 -0
- hailer-0.1.1/src/hailer/models.py +262 -0
- hailer-0.1.1/src/hailer/notebooks.py +543 -0
- hailer-0.1.1/src/hailer/periods.py +208 -0
- hailer-0.1.1/src/hailer/prompts/__init__.py +0 -0
- hailer-0.1.1/src/hailer/prompts/system.md +130 -0
- hailer-0.1.1/src/hailer/secrets.py +117 -0
- hailer-0.1.1/src/hailer/session.py +113 -0
- hailer-0.1.1/src/hailer/web.py +340 -0
- hailer-0.1.1/tests/test_agent.py +717 -0
- hailer-0.1.1/tests/test_browser.py +83 -0
- hailer-0.1.1/tests/test_cli.py +1335 -0
- hailer-0.1.1/tests/test_config.py +489 -0
- hailer-0.1.1/tests/test_context.py +242 -0
- hailer-0.1.1/tests/test_log.py +141 -0
- hailer-0.1.1/tests/test_marimo_client.py +608 -0
- hailer-0.1.1/tests/test_mcp_server.py +722 -0
- hailer-0.1.1/tests/test_notebooks.py +409 -0
- hailer-0.1.1/tests/test_periods.py +174 -0
- hailer-0.1.1/tests/test_release.py +117 -0
- hailer-0.1.1/tests/test_secrets.py +133 -0
- hailer-0.1.1/tests/test_session.py +114 -0
- hailer-0.1.1/tests/test_web.py +251 -0
- hailer-0.1.1/uv.lock +1435 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Project context for Hailer
|
|
2
|
+
|
|
3
|
+
This folder holds the context, skills and prompts that Hailer gives to the AI agent for
|
|
4
|
+
**this** project. Commit it alongside the notebook so the whole team gets the same setup.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
.config/hailer/
|
|
8
|
+
├── hailer.toml (optional: the project config can live here instead of the repo root)
|
|
9
|
+
├── context/ always-on: every *.md here is sent to the model at the start of each session
|
|
10
|
+
├── skills/ on-demand: <name>/SKILL.md (+ reference/, scripts/) loaded only when needed
|
|
11
|
+
└── prompts/ reusable prompts: /prompt <name> [args]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## context/
|
|
15
|
+
|
|
16
|
+
Short Markdown files with facts the agent should always know: what the datasets are, column
|
|
17
|
+
meanings, conventions, house style. Files are loaded in filename order (prefix with `00-`,
|
|
18
|
+
`10-` to control it) and concatenated up to the `max_context_bytes` limit in `hailer.toml`.
|
|
19
|
+
|
|
20
|
+
**Everything in `context/` is sent to the configured model endpoint every session.** Do not
|
|
21
|
+
put credentials, customer data or anything you would not paste into a chat with the model.
|
|
22
|
+
Hailer warns at startup if a file looks like it contains a key or token.
|
|
23
|
+
|
|
24
|
+
## skills/
|
|
25
|
+
|
|
26
|
+
Skills use the Agent Skills format shared by Codex and Claude: a folder with a `SKILL.md`
|
|
27
|
+
whose frontmatter has `name` and `description`, plus optional `reference/` and `scripts/`.
|
|
28
|
+
Hailer puts only the name and description in the agent's instructions; the body is loaded
|
|
29
|
+
when the task matches (the agent calls `load_skill`) or when you type `/skill <name>`.
|
|
30
|
+
|
|
31
|
+
Write the `description` as the trigger: *when* to use the skill and *what* it produces.
|
|
32
|
+
|
|
33
|
+
## prompts/
|
|
34
|
+
|
|
35
|
+
Plain Markdown prompts. `{{args}}` is replaced with whatever follows the name:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
You > /prompt monthly-pack 2025-06
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Reloading
|
|
42
|
+
|
|
43
|
+
`/reload` re-reads this folder without restarting the session. `/context` shows what is loaded.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# PRA101 analysis: team context
|
|
2
|
+
|
|
3
|
+
> This file is sent to the model endpoint at the start of every Hailer session.
|
|
4
|
+
> Keep it factual and short. No credentials, no customer names, no row-level data.
|
|
5
|
+
|
|
6
|
+
## What the data is
|
|
7
|
+
|
|
8
|
+
- Monthly extracts of the PRA101 (capital+) return, one Parquet file per month in `data/`.
|
|
9
|
+
- Filenames encode the period as `YY-MM`, e.g. `25-03 pra101.parquet` is March 2025.
|
|
10
|
+
The data itself has **no** period column; Hailer adds one when loading several months.
|
|
11
|
+
- Later months may add columns. Treat a missing column in an older month as null, not an error.
|
|
12
|
+
|
|
13
|
+
## Column meanings
|
|
14
|
+
|
|
15
|
+
| column | meaning |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `exposure_class` | Regulatory exposure class: Corporate, Retail, Institution, Sovereign, Equity, Other |
|
|
18
|
+
| `counterparty_id` | Anonymised counterparty key; stable across months |
|
|
19
|
+
| `default_flag` | `true` when the exposure is in default. "Exclude defaults" means `default_flag == false` |
|
|
20
|
+
| `exposure_value` | Exposure at default in GBP |
|
|
21
|
+
| `rwa` | Risk-weighted assets in GBP |
|
|
22
|
+
| `risk_weight` | Effective risk weight (`rwa / exposure_value`); present from 2025-04 onwards |
|
|
23
|
+
|
|
24
|
+
## Conventions
|
|
25
|
+
|
|
26
|
+
- "Latest month" means the most recent period present in `data/`, not the calendar month.
|
|
27
|
+
- Month-on-month movement is `latest - previous`; percentages are relative to the previous month.
|
|
28
|
+
- Report RWA in £m with one decimal (e.g. £1,234.5m). Keep tables to the top 10 rows unless asked.
|
|
29
|
+
- Prefer Polars; use DuckDB SQL when joining across many monthly files.
|
|
30
|
+
- Charts: bar for breakdowns by class, line for trends over periods. Label axes with units.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Produce the monthly PRA101 pack for period {{args}} (use the latest period if none is given).
|
|
2
|
+
|
|
3
|
+
In the marimo notebook, add or update these sections, reusing existing variables and cells
|
|
4
|
+
where they already exist:
|
|
5
|
+
|
|
6
|
+
1. **Periods available**: a small table of the months found in `data/` and their row counts.
|
|
7
|
+
2. **Headline**: total RWA and exposure value for the period and the previous month, with
|
|
8
|
+
movement in £m and %.
|
|
9
|
+
3. **Movement by exposure class**: table sorted by absolute movement, plus a bar chart.
|
|
10
|
+
4. **Top 10 movers**: counterparties with the largest absolute RWA movement between the two
|
|
11
|
+
months (join on `counterparty_id`), showing both months and the movement.
|
|
12
|
+
5. **Data quality notes**: rows with null `exposure_class`, negative `rwa`, or
|
|
13
|
+
`exposure_value == 0`, counted per month.
|
|
14
|
+
|
|
15
|
+
Then reply in the terminal with a five-line summary: total movement, the largest driver, the
|
|
16
|
+
second-largest driver, anything unusual in the data-quality notes, and what you added to the
|
|
17
|
+
notebook. Do not paste tables into the terminal.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pra101-month-on-month
|
|
3
|
+
description: >-
|
|
4
|
+
Compare the two latest PRA101 months and explain what drives the RWA movement,
|
|
5
|
+
producing a movement table, an exposure-class breakdown and a chart in the marimo
|
|
6
|
+
notebook. Use when the user asks what changed, what is driving RWA, or for a
|
|
7
|
+
month-on-month comparison.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# PRA101 month-on-month movement
|
|
11
|
+
|
|
12
|
+
## When to use
|
|
13
|
+
|
|
14
|
+
The user asks any of: "compare the latest two months", "what's driving the RWA increase",
|
|
15
|
+
"break the movement down by exposure class", "show me the biggest movers".
|
|
16
|
+
|
|
17
|
+
## Steps
|
|
18
|
+
|
|
19
|
+
1. `list_periods` to confirm which months exist and whether columns differ between them.
|
|
20
|
+
2. In the notebook, reuse the existing `files`/`df` variables if a previous turn created them
|
|
21
|
+
(`notebook_cells` shows what exists). Otherwise create one cell that loads all months with
|
|
22
|
+
`load_periods(scan_period_files(DATA_DIR, "pra101"))` and keeps the `period` column.
|
|
23
|
+
3. Create (or edit) a cell `mom` that aggregates `rwa` and `exposure_value` by `period` and
|
|
24
|
+
`exposure_class`, pivots the latest two periods side by side, and adds `movement` and
|
|
25
|
+
`movement_pct` columns. Sort by absolute movement, descending.
|
|
26
|
+
4. Create a cell that shows `mom` with `mo.ui.table`, and a cell with a bar chart of
|
|
27
|
+
`movement` by `exposure_class` (Altair via `mo.ui.altair_chart`, or `df.plot`).
|
|
28
|
+
5. If the user says "exclude defaults", filter `default_flag == False` in the loading cell and
|
|
29
|
+
let the dependent cells re-run; do not create a second copy of the pipeline.
|
|
30
|
+
6. Run the checks in `reference/checks.md` before summarising.
|
|
31
|
+
7. In the terminal, give a two-sentence summary (largest driver, total movement in £m and %),
|
|
32
|
+
then list what was added to the notebook.
|
|
33
|
+
|
|
34
|
+
## Notebook hygiene
|
|
35
|
+
|
|
36
|
+
- One owning cell per public name (`files`, `df`, `mom`, `chart`); edit the owner, never redefine.
|
|
37
|
+
- Use `_` prefixed names for intermediates.
|
|
38
|
+
- Keep `hide_code=False` for analysis cells so the user can read the code.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Reconciliation checks for a month-on-month comparison
|
|
2
|
+
|
|
3
|
+
Run these in the scratchpad (not as notebook cells) before summarising. Print only the
|
|
4
|
+
check name and pass/fail with the numbers involved.
|
|
5
|
+
|
|
6
|
+
1. **Totals reconcile.** Sum of `movement` across exposure classes equals
|
|
7
|
+
`total_rwa(latest) - total_rwa(previous)` to within £1.
|
|
8
|
+
2. **No period leakage.** The `period` column only contains the two periods being compared.
|
|
9
|
+
3. **Class coverage.** Every `exposure_class` present in either month appears in the table;
|
|
10
|
+
a class missing from one month shows 0 for that month, not null.
|
|
11
|
+
4. **Defaults handled consistently.** If defaults are excluded, both months are filtered with
|
|
12
|
+
the same predicate (`default_flag == False`); state the number of rows excluded per month.
|
|
13
|
+
5. **Schema evolution.** If a column exists only in the latest month (e.g. `risk_weight`),
|
|
14
|
+
do not use it in the comparison unless the user asks; mention it once in the summary.
|
|
15
|
+
6. **Sign convention.** `movement > 0` means RWA increased in the latest month.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Close pull requests from outside the organization.
|
|
2
|
+
#
|
|
3
|
+
# GitHub has no setting that stops people opening pull requests on a public
|
|
4
|
+
# repository, so this closes pull requests from outsiders, leaving a short comment.
|
|
5
|
+
#
|
|
6
|
+
# A pull request is kept when its head branch lives in this repository (only people
|
|
7
|
+
# with write access can push one, so forks are the only route for outsiders) or when
|
|
8
|
+
# GitHub reports the author as OWNER, MEMBER or COLLABORATOR. The branch check comes
|
|
9
|
+
# first because the event payload only reports MEMBER for *public* org memberships;
|
|
10
|
+
# a member whose membership is private shows up as CONTRIBUTOR or NONE, which is
|
|
11
|
+
# how this workflow once closed a member's own pull request.
|
|
12
|
+
#
|
|
13
|
+
# pull_request_target runs on the base branch with a write token. It never checks
|
|
14
|
+
# out or runs code from the pull request, so that token is not exposed to it.
|
|
15
|
+
|
|
16
|
+
name: Members only
|
|
17
|
+
|
|
18
|
+
on:
|
|
19
|
+
pull_request_target:
|
|
20
|
+
types: [opened, reopened]
|
|
21
|
+
|
|
22
|
+
permissions:
|
|
23
|
+
pull-requests: write
|
|
24
|
+
|
|
25
|
+
jobs:
|
|
26
|
+
guard:
|
|
27
|
+
name: Close pull requests from non-members
|
|
28
|
+
if: ${{ github.event.pull_request.head.repo.full_name != github.repository && !contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association) }}
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
steps:
|
|
31
|
+
- name: Comment and close
|
|
32
|
+
env:
|
|
33
|
+
GH_TOKEN: ${{ github.token }}
|
|
34
|
+
PR: ${{ github.event.pull_request.number }}
|
|
35
|
+
run: |
|
|
36
|
+
gh pr comment "$PR" --repo "$GITHUB_REPOSITORY" --body "Thanks for the interest. This repository only accepts pull requests from members of the OpenAfterHours organization, so this one has been closed automatically. Please open an issue to discuss the change instead."
|
|
37
|
+
gh pr close "$PR" --repo "$GITHUB_REPOSITORY"
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Publish a release to PyPI.
|
|
2
|
+
#
|
|
3
|
+
# Triggered by the vX.Y.Z tag that `uv run python -m scripts.release` pushes after the
|
|
4
|
+
# tests pass locally. Runs the test suite again on every supported platform
|
|
5
|
+
# (test.yml) and builds the sdist and wheel; only if both succeed does it publish
|
|
6
|
+
# through the "pypi" environment using PyPI trusted publishing (OIDC, no API token
|
|
7
|
+
# to store) and then create the GitHub release with the distributions attached.
|
|
8
|
+
#
|
|
9
|
+
# PyPI's trusted publisher for this project is configured as:
|
|
10
|
+
# owner OpenAfterHours, repository hailer, workflow release.yml, environment pypi.
|
|
11
|
+
# Renaming this file or the environment breaks publishing.
|
|
12
|
+
|
|
13
|
+
name: Release
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
push:
|
|
17
|
+
tags:
|
|
18
|
+
- "v*"
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
test:
|
|
25
|
+
name: Tests
|
|
26
|
+
uses: ./.github/workflows/test.yml
|
|
27
|
+
|
|
28
|
+
build:
|
|
29
|
+
name: Build distributions
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
outputs:
|
|
32
|
+
version: ${{ steps.version.outputs.version }}
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v7
|
|
35
|
+
|
|
36
|
+
- uses: astral-sh/setup-uv@v10.1.0 # setup-uv publishes no floating major tag
|
|
37
|
+
with:
|
|
38
|
+
python-version: "3.12"
|
|
39
|
+
|
|
40
|
+
- name: Check that the tag matches the package version
|
|
41
|
+
id: version
|
|
42
|
+
run: |
|
|
43
|
+
version="$(uv version --short --color never)"
|
|
44
|
+
if [ "v${version}" != "${GITHUB_REF_NAME}" ]; then
|
|
45
|
+
echo "::error::tag ${GITHUB_REF_NAME} does not match the pyproject.toml version ${version}"
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
|
49
|
+
|
|
50
|
+
- name: Build sdist and wheel
|
|
51
|
+
run: uv build
|
|
52
|
+
|
|
53
|
+
- name: Check distribution metadata
|
|
54
|
+
run: uvx twine check --strict dist/*
|
|
55
|
+
|
|
56
|
+
- uses: actions/upload-artifact@v7
|
|
57
|
+
with:
|
|
58
|
+
name: dist
|
|
59
|
+
path: dist/
|
|
60
|
+
if-no-files-found: error
|
|
61
|
+
|
|
62
|
+
publish:
|
|
63
|
+
name: Publish to PyPI
|
|
64
|
+
needs: [test, build]
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
environment:
|
|
67
|
+
name: pypi
|
|
68
|
+
url: https://pypi.org/project/hailer/${{ needs.build.outputs.version }}/
|
|
69
|
+
permissions:
|
|
70
|
+
id-token: write # mint the OIDC token PyPI trusted publishing exchanges for an upload token
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/download-artifact@v8
|
|
73
|
+
with:
|
|
74
|
+
name: dist
|
|
75
|
+
path: dist/
|
|
76
|
+
|
|
77
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
78
|
+
|
|
79
|
+
github-release:
|
|
80
|
+
name: Create GitHub release
|
|
81
|
+
needs: [build, publish]
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
permissions:
|
|
84
|
+
contents: write
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/download-artifact@v8
|
|
87
|
+
with:
|
|
88
|
+
name: dist
|
|
89
|
+
path: dist/
|
|
90
|
+
|
|
91
|
+
- name: Create the release with the distributions attached
|
|
92
|
+
env:
|
|
93
|
+
GH_TOKEN: ${{ github.token }}
|
|
94
|
+
VERSION: ${{ needs.build.outputs.version }}
|
|
95
|
+
run: |
|
|
96
|
+
flags=()
|
|
97
|
+
case "$VERSION" in
|
|
98
|
+
*[a-z]*) flags+=(--prerelease) ;;
|
|
99
|
+
esac
|
|
100
|
+
gh release create "$GITHUB_REF_NAME" dist/* \
|
|
101
|
+
--repo "$GITHUB_REPOSITORY" \
|
|
102
|
+
--title "hailer $VERSION" \
|
|
103
|
+
--generate-notes \
|
|
104
|
+
"${flags[@]}"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Run the test suite.
|
|
2
|
+
#
|
|
3
|
+
# Runs on pushes to main and on pull requests, and is called by release.yml so a
|
|
4
|
+
# release cannot be published to PyPI unless the suite passes on every platform
|
|
5
|
+
# here. The suite is offline (no API key, marimo server, Codex process or network).
|
|
6
|
+
|
|
7
|
+
name: Tests
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
branches: [main]
|
|
12
|
+
pull_request:
|
|
13
|
+
workflow_call:
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
test:
|
|
20
|
+
name: pytest (${{ matrix.os }}, Python ${{ matrix.python }})
|
|
21
|
+
runs-on: ${{ matrix.os }}
|
|
22
|
+
strategy:
|
|
23
|
+
fail-fast: false
|
|
24
|
+
matrix:
|
|
25
|
+
os: [ubuntu-latest, windows-latest]
|
|
26
|
+
python: ["3.12", "3.13"]
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v7
|
|
29
|
+
|
|
30
|
+
- uses: astral-sh/setup-uv@v10.1.0 # setup-uv publishes no floating major tag
|
|
31
|
+
with:
|
|
32
|
+
python-version: ${{ matrix.python }}
|
|
33
|
+
enable-cache: true
|
|
34
|
+
|
|
35
|
+
- name: Install the project and the dev dependencies
|
|
36
|
+
run: uv sync --locked
|
|
37
|
+
|
|
38
|
+
- name: Run the tests
|
|
39
|
+
run: uv run --locked pytest
|
hailer-0.1.1/.gitignore
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
.ruff_cache/
|
|
7
|
+
.mypy_cache/
|
|
8
|
+
build/
|
|
9
|
+
dist/
|
|
10
|
+
|
|
11
|
+
# uv / virtualenv
|
|
12
|
+
.venv/
|
|
13
|
+
uv.lock.bak
|
|
14
|
+
|
|
15
|
+
# Hailer runtime state (thread ids, logs) — never commit
|
|
16
|
+
.hailer/
|
|
17
|
+
*.log
|
|
18
|
+
|
|
19
|
+
# Data files stay local
|
|
20
|
+
data/*
|
|
21
|
+
!data/.gitkeep
|
|
22
|
+
!data/README.md
|
|
23
|
+
|
|
24
|
+
# marimo
|
|
25
|
+
__marimo__/
|
|
26
|
+
.marimo/
|
|
27
|
+
|
|
28
|
+
# Secrets — never commit
|
|
29
|
+
.env
|
|
30
|
+
*.key
|
|
31
|
+
*.pem
|
|
32
|
+
|
|
33
|
+
# IDE / OS
|
|
34
|
+
.idea/
|
|
35
|
+
.vscode/
|
|
36
|
+
.DS_Store
|
|
37
|
+
Thumbs.db
|