dfm-python 0.1.0__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.
Files changed (66) hide show
  1. dfm_python-0.1.0/.cursor/worktrees.json +7 -0
  2. dfm_python-0.1.0/.github/workflows/database.yaml +67 -0
  3. dfm_python-0.1.0/.github/workflows/nowcast.yaml +69 -0
  4. dfm_python-0.1.0/.github/workflows/train.yaml +78 -0
  5. dfm_python-0.1.0/.gitignore +33 -0
  6. dfm_python-0.1.0/.python-version +1 -0
  7. dfm_python-0.1.0/MANIFEST.in +32 -0
  8. dfm_python-0.1.0/PKG-INFO +271 -0
  9. dfm_python-0.1.0/README.md +230 -0
  10. dfm_python-0.1.0/adapters/__init__.py +30 -0
  11. dfm_python-0.1.0/adapters/adapter_database.py +1280 -0
  12. dfm_python-0.1.0/assets/initialization.py +319 -0
  13. dfm_python-0.1.0/assets/test_initialization.py +404 -0
  14. dfm_python-0.1.0/config/default.yaml +33 -0
  15. dfm_python-0.1.0/config/series/default.yaml +133 -0
  16. dfm_python-0.1.0/config/series/test_series.yaml +134 -0
  17. dfm_python-0.1.0/config/test.yaml +33 -0
  18. dfm_python-0.1.0/database/__init__.py +113 -0
  19. dfm_python-0.1.0/database/client.py +80 -0
  20. dfm_python-0.1.0/database/db_utils.py +633 -0
  21. dfm_python-0.1.0/database/helpers.py +599 -0
  22. dfm_python-0.1.0/database/models.py +133 -0
  23. dfm_python-0.1.0/database/operations.py +1728 -0
  24. dfm_python-0.1.0/database/series.py +172 -0
  25. dfm_python-0.1.0/database/settings.py +324 -0
  26. dfm_python-0.1.0/database/spec_manager.py +323 -0
  27. dfm_python-0.1.0/migrations/001_initial_schema.sql +509 -0
  28. dfm_python-0.1.0/migrations/002_add_blocks.sql +179 -0
  29. dfm_python-0.1.0/pyproject.toml +57 -0
  30. dfm_python-0.1.0/scripts/__init__.py +2 -0
  31. dfm_python-0.1.0/scripts/ingest_api.py +659 -0
  32. dfm_python-0.1.0/scripts/nowcast_dfm.py +354 -0
  33. dfm_python-0.1.0/scripts/train_dfm.py +348 -0
  34. dfm_python-0.1.0/scripts/utils.py +194 -0
  35. dfm_python-0.1.0/services/__init__.py +20 -0
  36. dfm_python-0.1.0/services/api/__init__.py +15 -0
  37. dfm_python-0.1.0/services/api/base.py +99 -0
  38. dfm_python-0.1.0/services/api/bok_client.py +317 -0
  39. dfm_python-0.1.0/services/api/kosis_client.py +368 -0
  40. dfm_python-0.1.0/services/ingestion/__init__.py +14 -0
  41. dfm_python-0.1.0/services/ingestion/base.py +389 -0
  42. dfm_python-0.1.0/services/ingestion/bok.py +297 -0
  43. dfm_python-0.1.0/services/ingestion/kosis.py +298 -0
  44. dfm_python-0.1.0/services/ingestion/orchestrator.py +526 -0
  45. dfm_python-0.1.0/src/__init__.py +43 -0
  46. dfm_python-0.1.0/src/dfm_python/__init__.py +28 -0
  47. dfm_python-0.1.0/src/dfm_python/config.py +435 -0
  48. dfm_python-0.1.0/src/dfm_python/data_loader.py +500 -0
  49. dfm_python-0.1.0/src/dfm_python/dfm.py +2356 -0
  50. dfm_python-0.1.0/src/dfm_python/kalman.py +403 -0
  51. dfm_python-0.1.0/src/dfm_python/news.py +557 -0
  52. dfm_python-0.1.0/src/test/__init__.py +2 -0
  53. dfm_python-0.1.0/src/test/test_dfm.py +717 -0
  54. dfm_python-0.1.0/src/test/test_kalman.py +312 -0
  55. dfm_python-0.1.0/src/test/test_news.py +225 -0
  56. dfm_python-0.1.0/src/utils/__init__.py +12 -0
  57. dfm_python-0.1.0/src/utils/data_utils.py +148 -0
  58. dfm_python-0.1.0/tests/__init__.py +0 -0
  59. dfm_python-0.1.0/tests/test_adapters_database.py +184 -0
  60. dfm_python-0.1.0/tests/test_database_connection.py +213 -0
  61. dfm_python-0.1.0/tests/test_database_ingestion.py +508 -0
  62. dfm_python-0.1.0/tests/test_full_workflow.py +248 -0
  63. dfm_python-0.1.0/tests/test_nowcast_dfm.py +230 -0
  64. dfm_python-0.1.0/tests/test_nowcasting_dfm.py +148 -0
  65. dfm_python-0.1.0/tests/test_train_dfm.py +251 -0
  66. dfm_python-0.1.0/uv.lock +1247 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "setup-worktree": [
3
+ "source .venv/bin/activate && uv sync",
4
+ "cp $ROOT_WORKTREE_PATH/.env.local .env.local",
5
+ "set -a && source .env.local && set +a"
6
+ ]
7
+ }
@@ -0,0 +1,67 @@
1
+ name: Data Ingestion
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ spec_file:
7
+ description: 'Spec CSV file path (relative to src/spec/)'
8
+ required: false
9
+ default: '001_initial_spec.csv'
10
+ type: string
11
+
12
+ jobs:
13
+ ingest:
14
+ runs-on: ubuntu-latest
15
+ timeout-minutes: 60
16
+
17
+ steps:
18
+ - name: Checkout repository
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Set up Python
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version: '3.12'
25
+ cache: 'pip'
26
+
27
+ - name: Install dependencies
28
+ run: |
29
+ pip install uv
30
+ uv venv .venv
31
+ source .venv/bin/activate
32
+ uv sync
33
+
34
+ - name: Load environment variables
35
+ env:
36
+ SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
37
+ SUPABASE_SECRET_KEY: ${{ secrets.SUPABASE_SECRET_KEY }}
38
+ BOK_API_KEY: ${{ secrets.BOK_API_KEY }}
39
+ KOSIS_API_KEY: ${{ secrets.KOSIS_API_KEY }}
40
+ run: |
41
+ echo "SUPABASE_URL=$SUPABASE_URL" >> $GITHUB_ENV
42
+ echo "SUPABASE_SECRET_KEY=$SUPABASE_SECRET_KEY" >> $GITHUB_ENV
43
+ echo "BOK_API_KEY=$BOK_API_KEY" >> $GITHUB_ENV
44
+ echo "KOSIS_API_KEY=$KOSIS_API_KEY" >> $GITHUB_ENV
45
+
46
+ - name: Run data ingestion
47
+ env:
48
+ SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
49
+ SUPABASE_SECRET_KEY: ${{ secrets.SUPABASE_SECRET_KEY }}
50
+ BOK_API_KEY: ${{ secrets.BOK_API_KEY }}
51
+ KOSIS_API_KEY: ${{ secrets.KOSIS_API_KEY }}
52
+ GITHUB_WORKFLOW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
53
+ run: |
54
+ source .venv/bin/activate
55
+ python scripts/ingest_api.py
56
+ continue-on-error: false
57
+
58
+ - name: Upload logs on failure
59
+ if: failure()
60
+ uses: actions/upload-artifact@v4
61
+ with:
62
+ name: ingestion-logs
63
+ path: |
64
+ *.log
65
+ outputs/**/*.log
66
+ retention-days: 7
67
+
@@ -0,0 +1,69 @@
1
+ name: Nowcast DFM
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ config_path:
7
+ description: 'Model config CSV path (relative to src/spec/)'
8
+ required: false
9
+ default: 'src/spec/001_initial_spec.csv'
10
+ type: string
11
+
12
+ jobs:
13
+ nowcast:
14
+ runs-on: ubuntu-latest
15
+ timeout-minutes: 60
16
+
17
+ steps:
18
+ - name: Checkout repository
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Set up Python
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version: '3.12'
25
+ cache: 'pip'
26
+
27
+ - name: Install dependencies
28
+ run: |
29
+ pip install uv
30
+ uv venv .venv
31
+ source .venv/bin/activate
32
+ uv sync
33
+
34
+ - name: Download model weights (optional)
35
+ uses: actions/download-artifact@v4
36
+ with:
37
+ name: model-weights
38
+ path: .
39
+ continue-on-error: true
40
+
41
+ - name: Load environment variables
42
+ env:
43
+ SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
44
+ SUPABASE_SECRET_KEY: ${{ secrets.SUPABASE_SECRET_KEY }}
45
+ run: |
46
+ echo "SUPABASE_URL=$SUPABASE_URL" >> $GITHUB_ENV
47
+ echo "SUPABASE_SECRET_KEY=$SUPABASE_SECRET_KEY" >> $GITHUB_ENV
48
+
49
+ - name: Run nowcasting
50
+ env:
51
+ SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
52
+ SUPABASE_SECRET_KEY: ${{ secrets.SUPABASE_SECRET_KEY }}
53
+ run: |
54
+ source .venv/bin/activate
55
+ python scripts/nowcast_dfm.py \
56
+ "model.config_path=${{ inputs.config_path }}" \
57
+ "data.use_database=true" \
58
+ "data.strict_mode=false"
59
+
60
+ - name: Upload logs on failure
61
+ if: failure()
62
+ uses: actions/upload-artifact@v4
63
+ with:
64
+ name: nowcast-logs
65
+ path: |
66
+ *.log
67
+ outputs/**/*.log
68
+ retention-days: 7
69
+
@@ -0,0 +1,78 @@
1
+ name: Train DFM Model
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ config_path:
7
+ description: 'Model config CSV path (relative to src/spec/)'
8
+ required: false
9
+ default: 'src/spec/001_initial_spec.csv'
10
+ type: string
11
+ max_iter:
12
+ description: 'Maximum EM iterations for training'
13
+ required: false
14
+ default: '100'
15
+ type: string
16
+
17
+ jobs:
18
+ train:
19
+ runs-on: ubuntu-latest
20
+ timeout-minutes: 120
21
+
22
+ steps:
23
+ - name: Checkout repository
24
+ uses: actions/checkout@v4
25
+
26
+ - name: Set up Python
27
+ uses: actions/setup-python@v5
28
+ with:
29
+ python-version: '3.12'
30
+ cache: 'pip'
31
+
32
+ - name: Install dependencies
33
+ run: |
34
+ pip install uv
35
+ uv venv .venv
36
+ source .venv/bin/activate
37
+ uv sync
38
+
39
+ - name: Load environment variables
40
+ env:
41
+ SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
42
+ SUPABASE_SECRET_KEY: ${{ secrets.SUPABASE_SECRET_KEY }}
43
+ run: |
44
+ echo "SUPABASE_URL=$SUPABASE_URL" >> $GITHUB_ENV
45
+ echo "SUPABASE_SECRET_KEY=$SUPABASE_SECRET_KEY" >> $GITHUB_ENV
46
+
47
+ - name: Train DFM model
48
+ env:
49
+ SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
50
+ SUPABASE_SECRET_KEY: ${{ secrets.SUPABASE_SECRET_KEY }}
51
+ run: |
52
+ source .venv/bin/activate
53
+ python scripts/train_dfm.py \
54
+ "model.config_path=${{ inputs.config_path }}" \
55
+ "data.use_database=true" \
56
+ "data.strict_mode=false" \
57
+ "dfm.max_iter=${{ inputs.max_iter }}"
58
+
59
+ - name: Upload model weights
60
+ if: success()
61
+ uses: actions/upload-artifact@v4
62
+ with:
63
+ name: model-weights
64
+ path: |
65
+ ResDFM.pkl
66
+ *.pkl
67
+ retention-days: 30
68
+
69
+ - name: Upload logs on failure
70
+ if: failure()
71
+ uses: actions/upload-artifact@v4
72
+ with:
73
+ name: train-logs
74
+ path: |
75
+ *.log
76
+ outputs/**/*.log
77
+ retention-days: 7
78
+
@@ -0,0 +1,33 @@
1
+ .env
2
+ .env.local
3
+ .env.*
4
+ *.env
5
+
6
+ .vscode/
7
+
8
+ # Secrets and credentials
9
+ secrets/
10
+ credentials/
11
+ *.key
12
+ *.pem
13
+ *.p12
14
+ *.pfx
15
+
16
+ # Excel files
17
+ *.xls
18
+ *.xlsx
19
+
20
+ # MATLAB files
21
+ *.mat
22
+ data/**/*.xls
23
+ data/**/*.xlsx
24
+ matlab/**/*.xls
25
+ *.m
26
+
27
+ *.txt
28
+ *.pyc
29
+
30
+ outputs/PR_REVIEW.md
31
+ PR_DESCRIPTION.md
32
+
33
+ outputs/
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,32 @@
1
+ # Include only the DFM module (src/nowcasting)
2
+ # Exclude all application-specific code (adapters, database, scripts, config, etc.)
3
+
4
+ include README.md
5
+ include LICENSE
6
+ include pyproject.toml
7
+
8
+ # Include only the nowcasting package
9
+ recursive-include src/nowcasting *.py
10
+
11
+ # Exclude application-specific directories
12
+ prune adapters
13
+ prune database
14
+ prune scripts
15
+ prune services
16
+ prune config
17
+ prune migrations
18
+ prune tests
19
+ prune assets
20
+
21
+ # Include README (already specified above, but be explicit)
22
+ include README.md
23
+
24
+ # Exclude other application-specific files (but keep README.md)
25
+ exclude *.yaml
26
+ exclude *.yml
27
+ exclude *.csv
28
+ exclude *.sql
29
+ exclude *.lock
30
+ exclude .gitignore
31
+ exclude .env*
32
+
@@ -0,0 +1,271 @@
1
+ Metadata-Version: 2.4
2
+ Name: dfm-python
3
+ Version: 0.1.0
4
+ Summary: Dynamic Factor Model (DFM) estimation and nowcasting in Python
5
+ Project-URL: Homepage, https://github.com/yourusername/dfm-python
6
+ Project-URL: Documentation, https://github.com/yourusername/dfm-python#readme
7
+ Project-URL: Repository, https://github.com/yourusername/dfm-python
8
+ Project-URL: Issues, https://github.com/yourusername/dfm-python/issues
9
+ Author: DFM Python Contributors
10
+ License: MIT
11
+ Keywords: dfm,dynamic-factor-model,econometrics,forecasting,nowcasting,time-series
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Requires-Python: >=3.12
19
+ Requires-Dist: numpy>=1.24.0
20
+ Requires-Dist: pandas>=2.0.0
21
+ Requires-Dist: scipy>=1.10.0
22
+ Provides-Extra: all
23
+ Requires-Dist: httpx>=0.24.0; extra == 'all'
24
+ Requires-Dist: hydra-core>=1.3.0; extra == 'all'
25
+ Requires-Dist: omegaconf>=2.3.0; extra == 'all'
26
+ Requires-Dist: psycopg2-binary>=2.9.0; extra == 'all'
27
+ Requires-Dist: pydantic-settings>=2.11.0; extra == 'all'
28
+ Requires-Dist: pytest>=7.0.0; extra == 'all'
29
+ Requires-Dist: supabase>=2.0.0; extra == 'all'
30
+ Provides-Extra: database
31
+ Requires-Dist: httpx>=0.24.0; extra == 'database'
32
+ Requires-Dist: psycopg2-binary>=2.9.0; extra == 'database'
33
+ Requires-Dist: pydantic-settings>=2.11.0; extra == 'database'
34
+ Requires-Dist: supabase>=2.0.0; extra == 'database'
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
37
+ Provides-Extra: hydra
38
+ Requires-Dist: hydra-core>=1.3.0; extra == 'hydra'
39
+ Requires-Dist: omegaconf>=2.3.0; extra == 'hydra'
40
+ Description-Content-Type: text/markdown
41
+
42
+ # dfm-python
43
+
44
+ A generic Python implementation of Dynamic Factor Models (DFM) for nowcasting and forecasting.
45
+
46
+ ## Features
47
+
48
+ - **Generic DFM Estimation**: EM algorithm for estimating dynamic factor models
49
+ - **Mixed-Frequency Data**: Handle data with different frequencies (daily, monthly, quarterly)
50
+ - **Missing Data Handling**: Robust handling of missing observations
51
+ - **News Decomposition**: Compare forecasts between vintages and attribute changes
52
+ - **Flexible Configuration**: Support for YAML and CSV configuration files
53
+ - **Optional Hydra Integration**: Use Hydra for experiment management (optional dependency)
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ pip install dfm-python
59
+ ```
60
+
61
+ ### Optional Dependencies
62
+
63
+ For Hydra configuration support:
64
+ ```bash
65
+ pip install dfm-python[hydra]
66
+ ```
67
+
68
+ For database integration (application-specific):
69
+ ```bash
70
+ pip install dfm-python[database]
71
+ ```
72
+
73
+ Install all optional dependencies:
74
+ ```bash
75
+ pip install dfm-python[all]
76
+ ```
77
+
78
+ ## Quick Start
79
+
80
+ ### Basic Usage
81
+
82
+ ```python
83
+ from dfm_python import load_config, load_data, dfm
84
+ import pandas as pd
85
+
86
+ # Load configuration from YAML or CSV
87
+ config = load_config('config.yaml')
88
+
89
+ # Load data from CSV file
90
+ X, Time, Z = load_data('data.csv', config, sample_start=pd.Timestamp('2000-01-01'))
91
+
92
+ # Estimate DFM
93
+ Res = dfm(X, config, threshold=1e-4)
94
+
95
+ # Access results
96
+ factors = Res.Z # Extracted factors
97
+ loadings = Res.C # Factor loadings
98
+ ```
99
+
100
+ ### Configuration
101
+
102
+ The DFM module uses a configuration object (`DFMConfig`) that defines:
103
+
104
+ - **Series**: Each time series with frequency, transformation, and block membership
105
+ - **Blocks**: Factor blocks with number of factors per block
106
+ - **Estimation Parameters**: EM algorithm settings (threshold, max_iter, etc.)
107
+
108
+ #### YAML Configuration Example
109
+
110
+ ```yaml
111
+ # config.yaml
112
+ model:
113
+ series:
114
+ series_1:
115
+ frequency: "m"
116
+ transformation: "pch"
117
+ blocks: [Global]
118
+ series_2:
119
+ frequency: "q"
120
+ transformation: "pca"
121
+ blocks: [Global, Investment]
122
+
123
+ blocks:
124
+ Global:
125
+ factors: 1
126
+ Investment:
127
+ factors: 1
128
+
129
+ dfm:
130
+ ar_lag: 1
131
+ threshold: 1e-5
132
+ max_iter: 5000
133
+ ```
134
+
135
+ #### CSV Configuration Example
136
+
137
+ ```csv
138
+ SeriesID,Frequency,Transformation,Block_Global,Block_Investment
139
+ series_1,m,pch,1,0
140
+ series_2,q,pca,1,1
141
+ ```
142
+
143
+ ### Data Format
144
+
145
+ CSV data files should have:
146
+ - First column: Date (YYYY-MM-DD format)
147
+ - Subsequent columns: One per series (matching SeriesID from config)
148
+ - Column names: Match SeriesID from configuration
149
+
150
+ Example:
151
+ ```csv
152
+ Date,series_1,series_2
153
+ 2000-01-01,100.0,50.0
154
+ 2000-02-01,101.0,51.0
155
+ ...
156
+ ```
157
+
158
+ ## Core Components
159
+
160
+ ### DFMConfig
161
+
162
+ Configuration dataclass that defines the model structure:
163
+
164
+ ```python
165
+ from dfm_python import DFMConfig, SeriesConfig
166
+
167
+ config = DFMConfig(
168
+ series=[
169
+ SeriesConfig(
170
+ series_id="gdp",
171
+ frequency="q",
172
+ transformation="pca",
173
+ blocks=[1, 0] # Binary: loads on block 0 (Global)
174
+ )
175
+ ],
176
+ block_names=["Global", "Investment"],
177
+ factors_per_block=[1, 1],
178
+ ar_lag=1,
179
+ threshold=1e-5,
180
+ max_iter=5000
181
+ )
182
+ ```
183
+
184
+ ### DFM Estimation
185
+
186
+ ```python
187
+ from dfm_python import dfm
188
+
189
+ # Estimate DFM model
190
+ result = dfm(X, config, threshold=1e-4, max_iter=1000)
191
+
192
+ # Access results
193
+ factors = result.Z # Factor estimates (T x r)
194
+ loadings = result.C # Factor loadings (N x r)
195
+ transition = result.A # Transition matrix
196
+ covariance = result.Q # Factor covariance
197
+ ```
198
+
199
+ ### News Decomposition
200
+
201
+ Compare forecasts between two vintages:
202
+
203
+ ```python
204
+ from dfm_python import update_nowcast
205
+
206
+ # Compare old vs new vintage
207
+ news_df, forecast_df = update_nowcast(
208
+ X_old, X_new, Time, config, result,
209
+ series="gdp",
210
+ vintage_old="2024-01-01",
211
+ vintage_new="2024-02-01"
212
+ )
213
+ ```
214
+
215
+ ## API Reference
216
+
217
+ ### Main Functions
218
+
219
+ - `load_config(file)`: Load configuration from YAML or CSV
220
+ - `load_data(file, config)`: Load and transform data from CSV
221
+ - `dfm(X, config)`: Estimate DFM model using EM algorithm
222
+ - `update_nowcast(...)`: Perform news decomposition between vintages
223
+
224
+ ### Configuration
225
+
226
+ - `DFMConfig`: Main configuration class
227
+ - `SeriesConfig`: Individual series configuration
228
+
229
+ ### Results
230
+
231
+ - `DFMResult`: Result object with factors, loadings, and model parameters
232
+
233
+ ## Architecture
234
+
235
+ The DFM module is designed to be **generic and application-agnostic**:
236
+
237
+ - **Core Module** (`dfm_python`): Generic DFM estimation logic
238
+ - **Adapters** (application-specific): Database integration, API clients, etc.
239
+
240
+ This separation allows the core module to be used in any context while keeping application-specific code separate.
241
+
242
+ ## Requirements
243
+
244
+ - Python >= 3.12
245
+ - numpy >= 1.24.0
246
+ - pandas >= 2.0.0
247
+ - scipy >= 1.10.0
248
+
249
+ ## License
250
+
251
+ MIT License
252
+
253
+ ## Contributing
254
+
255
+ Contributions are welcome! Please ensure that:
256
+ - Core module remains generic (no application-specific code)
257
+ - All optional dependencies are properly handled
258
+ - Tests pass for the core functionality
259
+
260
+ ## Citation
261
+
262
+ If you use this package in your research, please cite:
263
+
264
+ ```bibtex
265
+ @software{dfm-python,
266
+ title = {dfm-python: Dynamic Factor Models for Nowcasting},
267
+ author = {DFM Python Contributors},
268
+ year = {2024},
269
+ url = {https://github.com/yourusername/dfm-python}
270
+ }
271
+ ```