microdf-python 1.1.1__py3-none-any.whl

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.
@@ -0,0 +1,395 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+ import microdf as mdf
5
+ from microdf.microdataframe import MicroDataFrame
6
+ from microdf.microseries import MicroSeries
7
+
8
+
9
+ def test_df_init() -> None:
10
+ arr = np.array([0, 1, 1])
11
+ w = np.array([3, 0, 9])
12
+ df = mdf.MicroDataFrame({"a": arr}, weights=w)
13
+ assert df.a.mean() == np.average(arr, weights=w)
14
+
15
+ df = mdf.MicroDataFrame()
16
+ df["a"] = arr
17
+ df.set_weights(w)
18
+ assert df.a.mean() == np.average(arr, weights=w)
19
+
20
+ df = mdf.MicroDataFrame()
21
+ df["a"] = arr
22
+ df["w"] = w
23
+ df.set_weight_col("w")
24
+ assert df.a.mean() == np.average(arr, weights=w)
25
+
26
+ # Test set_weights with string (column name)
27
+ df2 = mdf.MicroDataFrame()
28
+ df2["a"] = arr
29
+ df2["w"] = w
30
+ df2.set_weights("w") # Using string column name instead of set_weight_col
31
+ assert df2.a.mean() == np.average(arr, weights=w)
32
+ assert np.array_equal(df2.weights.values, w)
33
+
34
+
35
+ def test_handles_empty_index() -> None:
36
+ arr = np.array([0, 1, 1])
37
+ w = np.array([3, 0, 9])
38
+ df = mdf.MicroDataFrame({"a": arr}, weights=w)
39
+
40
+ empty_index = pd.Index([])
41
+ df[empty_index] # Implicit assert; checking for ValueError
42
+
43
+
44
+ def test_series_getitem() -> None:
45
+ arr = np.array([0, 1, 1])
46
+ w = np.array([3, 0, 9])
47
+ s = mdf.MicroSeries(arr, weights=w)
48
+ assert s[[1, 2]].sum() == np.sum(arr[[1, 2]] * w[[1, 2]])
49
+
50
+ assert s[1:3].sum() == np.sum(arr[1:3] * w[1:3])
51
+
52
+
53
+ def test_sum() -> None:
54
+ arr = np.array([0, 1, 1])
55
+ w = np.array([3, 0, 9])
56
+ series = mdf.MicroSeries(arr, weights=w)
57
+ assert series.sum() == (arr * w).sum()
58
+
59
+ arr = np.linspace(-20, 100, 100)
60
+ w = np.linspace(1, 3, 100)
61
+ series = mdf.MicroSeries(arr)
62
+ series.set_weights(w)
63
+ assert series.sum() == (arr * w).sum()
64
+
65
+ # Verify that an error is thrown when passing weights of different size
66
+ # from the values.
67
+ w = np.linspace(1, 3, 101)
68
+ series = mdf.MicroSeries(arr)
69
+ try:
70
+ series.set_weights(w)
71
+ assert False
72
+ except Exception:
73
+ pass
74
+
75
+
76
+ def test_mean() -> None:
77
+ arr = np.array([3, 0, 2])
78
+ w = np.array([4, 1, 1])
79
+ series = mdf.MicroSeries(arr, weights=w)
80
+ assert series.mean() == np.average(arr, weights=w)
81
+
82
+ arr = np.linspace(-20, 100, 100)
83
+ w = np.linspace(1, 3, 100)
84
+ series = mdf.MicroSeries(arr)
85
+ series.set_weights(w)
86
+ assert series.mean() == np.average(arr, weights=w)
87
+
88
+ w = np.linspace(1, 3, 101)
89
+ series = mdf.MicroSeries(arr)
90
+ try:
91
+ series.set_weights(w)
92
+ assert False
93
+ except Exception:
94
+ pass
95
+
96
+
97
+ def test_poverty_count() -> None:
98
+ arr = np.array([10000, 20000, 50000])
99
+ w = np.array([1123, 1144, 2211])
100
+ df = pd.DataFrame()
101
+ df["income"] = arr
102
+ df["threshold"] = 16000
103
+ df = MicroDataFrame(df, weights=w)
104
+ assert df.poverty_count("income", "threshold") == w[0]
105
+
106
+
107
+ def test_median() -> None:
108
+ # 1, 2, 3, 4, *4*, 4, 5, 5, 5
109
+ arr = np.array([1, 2, 3, 4, 5])
110
+ w = np.array([1, 1, 1, 3, 3])
111
+ series = mdf.MicroSeries(arr, weights=w)
112
+ assert series.median() == 4
113
+
114
+
115
+ def test_weighted_quantile_skewed() -> None:
116
+ # 99% of the population has 0 income, 1% has 1M
117
+ # The median should be 0, not an interpolated value
118
+ series = mdf.MicroSeries([0, 1_000_000], weights=[99, 1])
119
+ assert series.median() == 0
120
+ assert series.quantile(0.5) == 0
121
+ # 99th percentile is still 0 since exactly 99% have 0
122
+ assert series.quantile(0.99) == 0
123
+ # Only quantile > 0.99 gives 1M
124
+ assert series.quantile(1.0) == 1_000_000
125
+ # Test multiple quantiles
126
+ result = series.quantile([0.1, 0.5, 0.99, 1.0])
127
+ assert result[0.1] == 0
128
+ assert result[0.5] == 0
129
+ assert result[0.99] == 0
130
+ assert result[1.0] == 1_000_000
131
+
132
+
133
+ def test_weighted_quantile_boundaries() -> None:
134
+ # Test q=0 returns minimum, q=1 returns maximum
135
+ series = mdf.MicroSeries([10, 20, 30], weights=[1, 1, 1])
136
+ assert series.quantile(0.0) == 10
137
+ assert series.quantile(1.0) == 30
138
+
139
+
140
+ def test_weighted_quantile_equal_weights() -> None:
141
+ # With equal weights, should match "replicated" interpretation
142
+ # Values: 1, 2, 3 each with weight 2 -> like [1,1,2,2,3,3]
143
+ series = mdf.MicroSeries([1, 2, 3], weights=[2, 2, 2])
144
+ # cumsum_normalized = [2/6, 4/6, 6/6] = [0.333, 0.667, 1.0]
145
+ # median (0.5): smallest where cumsum >= 0.5 -> index 1 -> value 2
146
+ assert series.median() == 2
147
+ # 0.25 quantile: smallest where cumsum >= 0.25 -> index 0 -> value 1
148
+ assert series.quantile(0.25) == 1
149
+ # 0.75 quantile: smallest where cumsum >= 0.75 -> index 2 -> value 3
150
+ assert series.quantile(0.75) == 3
151
+
152
+
153
+ def test_weighted_quantile_unsorted_input() -> None:
154
+ # Ensure sorting works correctly
155
+ series = mdf.MicroSeries([30, 10, 20], weights=[1, 2, 1])
156
+ # Sorted: values [10, 20, 30], weights [2, 1, 1]
157
+ # cumsum_normalized = [0.5, 0.75, 1.0]
158
+ assert series.quantile(0.0) == 10
159
+ assert series.quantile(0.5) == 10 # cumsum[0]=0.5 >= 0.5
160
+ assert series.quantile(0.6) == 20 # cumsum[1]=0.75 >= 0.6
161
+ assert series.quantile(1.0) == 30
162
+
163
+
164
+ def test_unweighted_groupby() -> None:
165
+ df = mdf.MicroDataFrame({"x": [1, 2], "y": [3, 4], "z": [5, 6]})
166
+ assert (df.groupby("x").z.sum().values == np.array([5.0, 6.0])).all()
167
+
168
+
169
+ def test_multiple_groupby() -> None:
170
+ df = mdf.MicroDataFrame({"x": [1, 2], "y": [3, 4], "z": [5, 6]})
171
+ assert (df.groupby(["x", "y"]).z.sum() == np.array([5, 6])).all()
172
+
173
+
174
+ def test_set_index() -> None:
175
+ d = mdf.MicroDataFrame(dict(x=[1, 2, 3]), weights=[4, 5, 6])
176
+ assert d.x.__class__ == MicroSeries
177
+ d.index = [1, 2, 3]
178
+ assert d.x.__class__ == MicroSeries
179
+
180
+
181
+ def test_reset_index() -> None:
182
+ d = mdf.MicroDataFrame(dict(x=[1, 2, 3]), weights=[4, 5, 6])
183
+ assert d.reset_index().__class__ == MicroDataFrame
184
+
185
+
186
+ def test_cumsum() -> None:
187
+ s = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
188
+ assert np.array_equal(s.cumsum().values, [4, 14, 32])
189
+
190
+ s = mdf.MicroSeries([2, 1, 3], weights=[5, 4, 6])
191
+ assert np.array_equal(s.cumsum().values, [10, 14, 32])
192
+
193
+ s = mdf.MicroSeries([3, 1, 2], weights=[6, 4, 5])
194
+ assert np.array_equal(s.cumsum().values, [18, 22, 32])
195
+
196
+
197
+ def test_rank() -> None:
198
+ s = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
199
+ assert np.array_equal(s.rank().values, [4, 9, 15])
200
+
201
+ s = mdf.MicroSeries([3, 1, 2], weights=[6, 4, 5])
202
+ assert np.array_equal(s.rank().values, [15, 4, 9])
203
+
204
+ s = mdf.MicroSeries([2, 1, 3], weights=[5, 4, 6])
205
+ assert np.array_equal(s.rank().values, [9, 4, 15])
206
+
207
+
208
+ def test_percentile_rank() -> None:
209
+ s = mdf.MicroSeries([4, 2, 3, 1], weights=[20, 40, 20, 20])
210
+ assert np.array_equal(s.percentile_rank().values, [100, 60, 80, 20])
211
+
212
+
213
+ def test_quartile_rank() -> None:
214
+ s = mdf.MicroSeries([4, 2, 3], weights=[25, 50, 25])
215
+ assert np.array_equal(s.quartile_rank().values, [4, 2, 3])
216
+
217
+
218
+ def test_quintile_rank() -> None:
219
+ s = mdf.MicroSeries([4, 2, 3], weights=[20, 60, 20])
220
+ assert np.array_equal(s.quintile_rank().values, [5, 3, 4])
221
+
222
+
223
+ def test_decile_rank() -> None:
224
+ s = mdf.MicroSeries(
225
+ [5, 4, 3, 2, 1, 6, 7, 8, 9],
226
+ weights=[10, 20, 10, 10, 10, 10, 10, 10, 10],
227
+ )
228
+ assert np.array_equal(s.decile_rank().values, [6, 5, 3, 2, 1, 7, 8, 9, 10])
229
+
230
+
231
+ def test_copy_equals() -> None:
232
+ d = mdf.MicroDataFrame(
233
+ {"x": [1, 2], "y": [3, 4], "z": [5, 6]}, weights=[7, 8]
234
+ )
235
+ d_copy = d.copy()
236
+ d_copy_diff_weights = d_copy.copy()
237
+ d_copy_diff_weights.weights *= 2
238
+ assert d.equals(d_copy)
239
+ assert not d.equals(d_copy_diff_weights)
240
+ # Same for a MicroSeries.
241
+ assert d.x.equals(d_copy.x)
242
+ assert not d.x.equals(d_copy_diff_weights.x)
243
+
244
+
245
+ def test_subset() -> None:
246
+ df = mdf.MicroDataFrame(
247
+ {"x": [1, 2], "y": [3, 4], "z": [5, 6]}, weights=[7, 8]
248
+ )
249
+ df_no_z = mdf.MicroDataFrame({"x": [1, 2], "y": [3, 4]}, weights=[7, 8])
250
+ assert df[["x", "y"]].equals(df_no_z)
251
+ df_no_z_diff_weights = df_no_z.copy()
252
+ df_no_z_diff_weights.weights += 1
253
+ assert not df[["x", "y"]].equals(df_no_z_diff_weights)
254
+
255
+
256
+ def test_value_subset() -> None:
257
+ d = mdf.MicroDataFrame({"x": [1, 2, 3], "y": [1, 2, 2]}, weights=[4, 5, 6])
258
+ d2 = d[d.y > 1]
259
+ assert d2.y.shape == d2.weights.shape
260
+
261
+
262
+ def test_bitwise_ops_return_microseries() -> None:
263
+ s1 = mdf.MicroSeries([True, False, True], weights=[1, 2, 3])
264
+ s2 = mdf.MicroSeries([False, False, True], weights=[1, 2, 3])
265
+ and_result = s1 & s2
266
+ or_result = s1 | s2
267
+ assert isinstance(and_result, mdf.MicroSeries)
268
+ assert isinstance(or_result, mdf.MicroSeries)
269
+ expected_and = mdf.MicroSeries([False, False, True], weights=[1, 2, 3])
270
+ expected_or = mdf.MicroSeries([True, False, True], weights=[1, 2, 3])
271
+ assert and_result.equals(expected_and)
272
+ assert or_result.equals(expected_or)
273
+
274
+
275
+ def test_additional_ops_return_microseries() -> None:
276
+ s = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
277
+ radd = 1 + s
278
+ xor = s ^ mdf.MicroSeries([0, 1, 0], weights=[4, 5, 6])
279
+ inv = ~mdf.MicroSeries([True, False], weights=[1, 1])
280
+ assert isinstance(radd, mdf.MicroSeries)
281
+ assert isinstance(xor, mdf.MicroSeries)
282
+ assert isinstance(inv, mdf.MicroSeries)
283
+
284
+
285
+ def test_reset_index_inplace() -> None:
286
+ df = pd.DataFrame(
287
+ {"A": [1, 2, 3, 4], "B": [5, 6, 7, 8]}, index=["a", "b", "c", "d"]
288
+ )
289
+ weights = np.array([0.1, 0.2, 0.3, 0.4])
290
+ mdf = MicroDataFrame(df, weights=weights)
291
+
292
+ # Test 1: reset_index with inplace=False (default)
293
+ mdf_copy = mdf.copy()
294
+ result = mdf_copy.reset_index()
295
+ assert list(mdf_copy.index) == ["a", "b", "c", "d"]
296
+ assert list(result.index) == [0, 1, 2, 3]
297
+ assert "index" in result.columns
298
+ assert list(result["index"]) == ["a", "b", "c", "d"]
299
+ np.testing.assert_array_equal(result.weights.values, weights)
300
+
301
+ # Test 2: reset_index with inplace=True
302
+ mdf_copy = mdf.copy()
303
+ result = mdf_copy.reset_index(inplace=True)
304
+ assert result is None
305
+ assert list(mdf_copy.index) == [0, 1, 2, 3]
306
+ assert "index" in mdf_copy.columns
307
+ assert list(mdf_copy["index"]) == ["a", "b", "c", "d"]
308
+ np.testing.assert_array_equal(mdf_copy.weights.values, weights)
309
+ assert isinstance(mdf_copy["A"], MicroSeries)
310
+ assert isinstance(mdf_copy["B"], MicroSeries)
311
+ assert isinstance(mdf_copy["index"], MicroSeries)
312
+
313
+ # Test 3: reset_index with drop=True
314
+ mdf_copy = mdf.copy()
315
+ mdf_copy.reset_index(drop=True, inplace=True)
316
+ assert list(mdf_copy.index) == [0, 1, 2, 3]
317
+ assert "index" not in mdf_copy.columns
318
+ assert list(mdf_copy.columns) == ["A", "B"]
319
+ np.testing.assert_array_equal(mdf_copy.weights.values, weights)
320
+
321
+ # Test 4: Multi-level index
322
+ arrays = [["bar", "bar", "baz", "baz"], ["one", "two", "one", "two"]]
323
+ multi_index = pd.MultiIndex.from_arrays(arrays, names=["first", "second"])
324
+ df_multi = pd.DataFrame(
325
+ {"A": [1, 2, 3, 4], "B": [5, 6, 7, 8]}, index=multi_index
326
+ )
327
+ mdf_multi = MicroDataFrame(df_multi, weights=weights)
328
+ result = mdf_multi.reset_index(level="first")
329
+ assert "first" in result.columns
330
+ assert result.index.name == "second"
331
+ np.testing.assert_array_equal(result.weights.values, weights)
332
+
333
+ # Reset all levels in place
334
+ mdf_multi.reset_index(inplace=True)
335
+ assert "first" in mdf_multi.columns
336
+ assert "second" in mdf_multi.columns
337
+ assert list(mdf_multi.index) == [0, 1, 2, 3]
338
+ np.testing.assert_array_equal(mdf_multi.weights.values, weights)
339
+
340
+
341
+ def test_loc_preserves_weights() -> None:
342
+ """Test that .loc[] returns MicroDataFrame with proper weights (issue
343
+ #265)."""
344
+ df = mdf.MicroDataFrame(
345
+ {"one": [1, 1, 1, 1, 1]}, weights=[10, 20, 30, 40, 50]
346
+ )
347
+
348
+ # Filter all rows (should get same weights)
349
+ filtered = df.loc[df.one == 1]
350
+ assert isinstance(filtered, MicroDataFrame)
351
+ assert filtered.one.sum() == 150.0 # Weighted sum
352
+
353
+ # Partial filter
354
+ df2 = mdf.MicroDataFrame(
355
+ {"x": [1, 2, 3, 4, 5]}, weights=[10, 20, 30, 40, 50]
356
+ )
357
+ subset = df2.loc[df2.x > 2]
358
+ assert isinstance(subset, MicroDataFrame)
359
+ assert subset.x.sum() == 500.0 # 3*30 + 4*40 + 5*50 = 500
360
+ np.testing.assert_array_equal(subset.weights.values, [30.0, 40.0, 50.0])
361
+
362
+
363
+ def test_iloc_preserves_weights() -> None:
364
+ """Test that .iloc[] returns MicroDataFrame with proper weights."""
365
+ df = mdf.MicroDataFrame(
366
+ {"x": [1, 2, 3, 4, 5]}, weights=[10, 20, 30, 40, 50]
367
+ )
368
+
369
+ # Select rows by position
370
+ subset = df.iloc[2:5]
371
+ assert isinstance(subset, MicroDataFrame)
372
+ assert subset.x.sum() == 500.0 # 3*30 + 4*40 + 5*50 = 500
373
+ np.testing.assert_array_equal(subset.weights.values, [30.0, 40.0, 50.0])
374
+
375
+
376
+ def test_groupby_column_selection() -> None:
377
+ """Test that groupby column selection preserves weights (issue #193)."""
378
+ d = mdf.MicroDataFrame(
379
+ dict(g=["a", "a", "b"], y=[1, 2, 3]), weights=[4, 5, 6]
380
+ )
381
+
382
+ # Test single column string selection
383
+ result_str = d.groupby("g")["y"].sum()
384
+ assert result_str["a"] == 14.0 # 1*4 + 2*5 = 14
385
+ assert result_str["b"] == 18.0 # 3*6 = 18
386
+
387
+ # Test list column selection
388
+ result_list = d.groupby("g")[["y"]].sum()
389
+ assert result_list.loc["a", "y"] == 14.0
390
+ assert result_list.loc["b", "y"] == 18.0
391
+
392
+ # Aggregated results should be plain DataFrame (no spurious weight column)
393
+ result_all = d.groupby("g").sum()
394
+ assert "weight" not in result_all.columns
395
+ assert list(result_all.columns) == ["y"]
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: microdf-python
3
+ Version: 1.1.1
4
+ Summary: Weighted pandas DataFrames and Series for survey microdata
5
+ Author-email: Max Ghenis <max@policyengine.org>
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: numpy
11
+ Requires-Dist: pandas
12
+ Provides-Extra: dev
13
+ Requires-Dist: codecov; extra == "dev"
14
+ Requires-Dist: flake8; extra == "dev"
15
+ Requires-Dist: flake8-pyproject; extra == "dev"
16
+ Requires-Dist: black; extra == "dev"
17
+ Requires-Dist: docformatter; extra == "dev"
18
+ Requires-Dist: isort; extra == "dev"
19
+ Requires-Dist: linecheck; extra == "dev"
20
+ Requires-Dist: pytest; extra == "dev"
21
+ Requires-Dist: pytest-cov; extra == "dev"
22
+ Requires-Dist: setuptools; extra == "dev"
23
+ Dynamic: license-file
24
+
25
+ [![Build](https://github.com/PolicyEngine/microdf/workflows/Pull%20request/badge.svg)](https://github.com/PolicyEngine/microdf/actions)
26
+ [![Codecov](https://codecov.io/gh/PolicyEngine/microdf/branch/master/graph/badge.svg)](https://codecov.io/gh/PolicyEngine/microdf)
27
+
28
+ # microdf
29
+ Weighted pandas DataFrames and Series for survey microdata analysis.
30
+
31
+ ## Overview
32
+ microdf provides `MicroDataFrame` and `MicroSeries` classes that extend pandas functionality with integrated weighting support, essential for accurate survey data analysis.
33
+
34
+ ## Key Features
35
+ - **MicroDataFrame**: A pandas DataFrame with an integrated weight column
36
+ - **MicroSeries**: A pandas Series with integrated weights
37
+ - **Weighted operations**: All aggregations (sum, mean, median, etc.) automatically use weights
38
+ - **Inequality metrics**: Built-in Gini coefficient calculation
39
+ - **Poverty analysis**: Integrated poverty rate and gap calculations
40
+
41
+ ## Installation
42
+ Install with:
43
+
44
+ pip install microdf-python
45
+
46
+ Or for development:
47
+
48
+ pip install git+https://github.com/PolicyEngine/microdf.git
49
+
50
+ ## Usage
51
+ ```python
52
+ import microdf as mdf
53
+ import pandas as pd
54
+
55
+ # Create sample data with weights
56
+ df = pd.DataFrame({
57
+ 'income': [10_000, 20_000, 30_000, 40_000, 50_000],
58
+ 'weights': [1, 2, 3, 2, 1]
59
+ })
60
+
61
+ # Create a MicroDataFrame
62
+ mdf_df = mdf.MicroDataFrame(df, weights='weights')
63
+
64
+ # All operations are weight-aware
65
+ print(mdf_df.income.mean()) # Weighted mean
66
+ print(mdf_df.income.gini()) # Gini coefficient
67
+ ```
68
+
69
+ ## Questions
70
+ Contact the maintainer, Max Ghenis (max@policyengine.org).
71
+
72
+ ## Citation
73
+ You may cite the source of your analysis as "microdf release #.#.#, author's calculations."
@@ -0,0 +1,10 @@
1
+ microdf/__init__.py,sha256=ldRvhE8t4cD7qlxQ5yLndMnWEUYpDw4m-jCDV_psXkY,319
2
+ microdf/microdataframe.py,sha256=X91fUvDS9xMzj2FFAoa6g9EhR_mJv60DXtvK4iIlMrM,33237
3
+ microdf/microseries.py,sha256=Z6ElPGlKikM3MwQo1aNbprsJW7ZqFZBRYk39C--XdJk,22719
4
+ microdf/tests/conftest.py,sha256=u-EMyX1-u_nM-YO0RJYCzYHQDXxUI2WQE6GkyJlErqg,150
5
+ microdf/tests/test_microseries_dataframe.py,sha256=alhJOVhPQIwNPaZNQDFCpIzRbljWgY-o2UWhGZ2OJvM,13388
6
+ microdf_python-1.1.1.dist-info/licenses/LICENSE,sha256=uPs-ASYnzlldpf2z8jeRgQFeEH3FLhSuX0rw0OKWoDU,1067
7
+ microdf_python-1.1.1.dist-info/METADATA,sha256=y0QTwJLvGE57pl3iF94_7R6CzMlwEgPQYV-KosS44y4,2420
8
+ microdf_python-1.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ microdf_python-1.1.1.dist-info/top_level.txt,sha256=T2WFPTygQQMdS3GF8YpZ12DKfMGrspbZ3r7z-e3KfiM,8
10
+ microdf_python-1.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Max Ghenis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ microdf