daybetter-services-python 1.0.7__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,7 @@
1
+ """DayBetter Python client library."""
2
+
3
+ from .client import DayBetterClient
4
+ from .exceptions import DayBetterError, AuthenticationError, APIError
5
+
6
+ __version__ = "1.0.7"
7
+ __all__ = ["DayBetterClient", "DayBetterError", "AuthenticationError", "APIError"]
@@ -0,0 +1,466 @@
1
+ """DayBetter API client."""
2
+
3
+ import aiohttp
4
+ import logging
5
+ from typing import Any, Dict, List, Optional, Tuple, Type
6
+
7
+ from .exceptions import DayBetterError, AuthenticationError, APIError
8
+
9
+ _LOGGER = logging.getLogger(__name__)
10
+
11
+
12
+ class DayBetterClient:
13
+ """DayBetter API client."""
14
+
15
+ # 测试环境URL
16
+ TEST_BASE_URL = "https://cloud.v2.dbiot.link/daybetter/hass/api/v1.0/"
17
+ # 正式环境URL
18
+ PROD_BASE_URL = "https://a.dbiot.org/daybetter/hass/api/v1.0/"
19
+
20
+ def __init__(
21
+ self,
22
+ token: str,
23
+ base_url: Optional[str] = None,
24
+ hass_code: Optional[str] = None
25
+ ):
26
+ """Initialize the client.
27
+
28
+ Args:
29
+ token: Authentication token
30
+ base_url: Base URL for the API (optional, will be determined by hass_code if not provided)
31
+ hass_code: Home Assistant integration code (optional, if provided and starts with "db-",
32
+ will use production environment)
33
+ """
34
+ self.token = token
35
+
36
+ # 根据 hass_code 或 base_url 确定使用的环境
37
+ if base_url is not None:
38
+ # 如果明确指定了 base_url,使用指定的 URL
39
+ self.base_url = base_url
40
+ elif hass_code is not None and hass_code.startswith("db-"):
41
+ # 如果 hass_code 以 "db-" 开头,使用正式环境
42
+ self.base_url = self.PROD_BASE_URL
43
+ _LOGGER.debug("Using production environment based on hass_code")
44
+ else:
45
+ # 默认使用测试环境
46
+ self.base_url = self.TEST_BASE_URL
47
+ _LOGGER.debug("Using test environment")
48
+
49
+ self._session: Optional[aiohttp.ClientSession] = None
50
+ self._auth_valid = True
51
+ self._devices: List[Dict[str, Any]] = []
52
+ self._pids: Dict[str, Any] = {}
53
+
54
+ async def __aenter__(self):
55
+ """Async context manager entry."""
56
+ self._session = aiohttp.ClientSession()
57
+ return self
58
+
59
+ async def __aexit__(
60
+ self,
61
+ exc_type: Optional[Type[BaseException]],
62
+ exc_val: Optional[BaseException],
63
+ exc_tb: Optional[Any],
64
+ ) -> None:
65
+ """Async context manager exit."""
66
+ if self._session:
67
+ await self._session.close()
68
+ self._session = None
69
+
70
+ def _get_session(self) -> aiohttp.ClientSession:
71
+ """Get or create aiohttp session."""
72
+ if not self._session:
73
+ self._session = aiohttp.ClientSession()
74
+ return self._session
75
+
76
+ def _get_headers(self) -> Dict[str, str]:
77
+ """Get request headers."""
78
+ return {"Authorization": f"Bearer {self.token}"}
79
+
80
+ async def fetch_devices(self) -> List[Dict[str, Any]]:
81
+ """Fetch devices from API.
82
+
83
+ Returns:
84
+ List of device dictionaries
85
+
86
+ Raises:
87
+ AuthenticationError: If authentication fails
88
+ APIError: If API request fails
89
+ """
90
+ try:
91
+ session = self._get_session()
92
+ url = f"{self.base_url}hass/devices"
93
+ headers = self._get_headers()
94
+
95
+ async with session.post(url, headers=headers) as resp:
96
+ if resp.status == 200:
97
+ data = await resp.json()
98
+ devices = data.get("data", [])
99
+ _LOGGER.debug("Fetched %d devices", len(devices))
100
+ self._auth_valid = True
101
+ return devices
102
+ elif resp.status == 401:
103
+ _LOGGER.error("Authentication failed - token may be expired")
104
+ self._auth_valid = False
105
+ raise AuthenticationError("Authentication failed - token may be expired")
106
+ else:
107
+ error_text = await resp.text()
108
+ _LOGGER.error("Failed to fetch devices: %s", error_text)
109
+ raise APIError(f"API error {resp.status}: {error_text}")
110
+ except aiohttp.ClientError as e:
111
+ _LOGGER.exception("Client error while fetching devices: %s", e)
112
+ raise APIError(f"Client error: {e}")
113
+ except Exception as e:
114
+ _LOGGER.exception("Exception while fetching devices: %s", e)
115
+ raise DayBetterError(f"Unexpected error: {e}")
116
+
117
+ async def fetch_pids(self) -> Dict[str, Any]:
118
+ """Fetch device type PIDs.
119
+
120
+ Returns:
121
+ Dictionary of device type PIDs
122
+
123
+ Raises:
124
+ AuthenticationError: If authentication fails
125
+ APIError: If API request fails
126
+ """
127
+ try:
128
+ session = self._get_session()
129
+ url = f"{self.base_url}hass/pids"
130
+ headers = self._get_headers()
131
+
132
+ async with session.post(url, headers=headers) as resp:
133
+ if resp.status == 200:
134
+ data = await resp.json()
135
+ self._auth_valid = True
136
+ return data.get("data", {})
137
+ elif resp.status == 401:
138
+ _LOGGER.error("Authentication failed - token may be expired")
139
+ self._auth_valid = False
140
+ raise AuthenticationError("Authentication failed - token may be expired")
141
+ else:
142
+ error_text = await resp.text()
143
+ _LOGGER.error("Failed to fetch PIDs: %s", error_text)
144
+ raise APIError(f"API error {resp.status}: {error_text}")
145
+ except aiohttp.ClientError as e:
146
+ _LOGGER.exception("Client error while fetching PIDs: %s", e)
147
+ raise APIError(f"Client error: {e}")
148
+ except Exception as e:
149
+ _LOGGER.exception("Exception while fetching PIDs: %s", e)
150
+ raise DayBetterError(f"Unexpected error: {e}")
151
+
152
+ async def control_device(
153
+ self,
154
+ device_name: str,
155
+ action: bool,
156
+ brightness: Optional[int] = None,
157
+ hs_color: Optional[Tuple[float, float]] = None,
158
+ color_temp: Optional[int] = None,
159
+ ) -> Dict[str, Any]:
160
+ """Control a device.
161
+
162
+ Args:
163
+ device_name: Name of the device to control
164
+ action: Switch action (True/False)
165
+ brightness: Brightness value (0-255)
166
+ hs_color: Hue and saturation tuple (hue, saturation)
167
+ color_temp: Color temperature in mireds
168
+
169
+ Returns:
170
+ Control result dictionary
171
+
172
+ Raises:
173
+ AuthenticationError: If authentication fails
174
+ APIError: If API request fails
175
+ """
176
+ session = self._get_session()
177
+ url = f"{self.base_url}hass/control"
178
+ headers = self._get_headers()
179
+
180
+ # Priority: color temperature > color > brightness > switch
181
+ if color_temp is not None:
182
+ # Convert mireds to Kelvin
183
+ kelvin = int(1000000 / color_temp)
184
+ payload = {
185
+ "deviceName": device_name,
186
+ "type": 4, # Type 4 is color temperature control
187
+ "kelvin": kelvin,
188
+ }
189
+ elif hs_color is not None:
190
+ h, s = hs_color
191
+ v = (brightness / 255) if brightness is not None else 1.0
192
+ payload = {
193
+ "deviceName": device_name,
194
+ "type": 3,
195
+ "hue": h,
196
+ "saturation": s / 100,
197
+ "brightness": v,
198
+ }
199
+ elif brightness is not None:
200
+ payload = {
201
+ "deviceName": device_name,
202
+ "type": 2,
203
+ "brightness": brightness
204
+ }
205
+ else:
206
+ # Type 1 control switch is used by default
207
+ payload = {
208
+ "deviceName": device_name,
209
+ "type": 1,
210
+ "on": action
211
+ }
212
+
213
+ try:
214
+ async with session.post(url, headers=headers, json=payload) as resp:
215
+ if resp.status == 200:
216
+ self._auth_valid = True
217
+ return await resp.json()
218
+ elif resp.status == 401:
219
+ _LOGGER.error("Authentication failed - token may be expired")
220
+ self._auth_valid = False
221
+ raise AuthenticationError("Authentication failed - token may be expired")
222
+ else:
223
+ error_text = await resp.text()
224
+ _LOGGER.error(
225
+ "Failed to control device %s: HTTP %d - %s",
226
+ device_name, resp.status, error_text
227
+ )
228
+ raise APIError(f"API error {resp.status}: {error_text}")
229
+ except aiohttp.ClientError as e:
230
+ _LOGGER.exception(
231
+ "Client error while controlling device %s: %s", device_name, e
232
+ )
233
+ raise APIError(f"Client error: {e}")
234
+ except Exception as e:
235
+ _LOGGER.exception(
236
+ "Exception while controlling device %s: %s", device_name, e
237
+ )
238
+ raise DayBetterError(f"Unexpected error: {e}")
239
+
240
+ async def fetch_mqtt_config(self) -> Dict[str, Any]:
241
+ """Fetch MQTT connection configuration.
242
+
243
+ Returns:
244
+ MQTT configuration dictionary
245
+
246
+ Raises:
247
+ AuthenticationError: If authentication fails
248
+ APIError: If API request fails
249
+ """
250
+ session = self._get_session()
251
+ url = f"{self.base_url}hass/cert"
252
+ headers = self._get_headers()
253
+ _LOGGER.debug("Requesting MQTT configuration URL: %s", url)
254
+
255
+ try:
256
+ async with session.post(url, headers=headers) as resp:
257
+ _LOGGER.debug("MQTT configuration API response status: %d", resp.status)
258
+
259
+ if resp.status == 200:
260
+ data = await resp.json()
261
+ _LOGGER.debug("MQTT configuration API raw response: %s", data)
262
+ self._auth_valid = True
263
+ return data.get("data", {})
264
+ elif resp.status == 401:
265
+ _LOGGER.error("Authentication failed - token may be expired")
266
+ self._auth_valid = False
267
+ raise AuthenticationError("Authentication failed - token may be expired")
268
+ else:
269
+ error_text = await resp.text()
270
+ _LOGGER.error("Failed to fetch MQTT config: %s", error_text)
271
+ raise APIError(f"API error {resp.status}: {error_text}")
272
+ except aiohttp.ClientError as e:
273
+ _LOGGER.exception("Client error while fetching MQTT config: %s", e)
274
+ raise APIError(f"Client error: {e}")
275
+ except Exception as e:
276
+ _LOGGER.exception("Exception while fetching MQTT config: %s", e)
277
+ raise DayBetterError(f"Unexpected error: {e}")
278
+
279
+ async def fetch_device_statuses(self) -> List[Dict[str, Any]]:
280
+ """Fetch statuses for all devices.
281
+
282
+ Returns:
283
+ List of device status dictionaries. Example item:
284
+ {
285
+ "deviceName": str,
286
+ "type": int,
287
+ "online": bool,
288
+ "temp": int,
289
+ "humi": int,
290
+ "bettery": int
291
+ }
292
+
293
+ Raises:
294
+ AuthenticationError: If authentication fails
295
+ APIError: If API request fails
296
+ """
297
+ try:
298
+ session = self._get_session()
299
+ url = f"{self.base_url}hass/status"
300
+ headers = self._get_headers()
301
+
302
+ async with session.post(url, headers=headers) as resp:
303
+ if resp.status == 200:
304
+ data = await resp.json()
305
+ self._auth_valid = True
306
+ # API expected to return { "data": [...] }
307
+ return data.get("data", [])
308
+ elif resp.status == 401:
309
+ _LOGGER.error("Authentication failed - token may be expired")
310
+ self._auth_valid = False
311
+ raise AuthenticationError("Authentication failed - token may be expired")
312
+ else:
313
+ error_text = await resp.text()
314
+ _LOGGER.error("Failed to fetch device statuses: %s", error_text)
315
+ raise APIError(f"API error {resp.status}: {error_text}")
316
+ except aiohttp.ClientError as e:
317
+ _LOGGER.exception("Client error while fetching device statuses: %s", e)
318
+ raise APIError(f"Client error: {e}")
319
+ except Exception as e:
320
+ _LOGGER.exception("Exception while fetching device statuses: %s", e)
321
+ raise DayBetterError(f"Unexpected error: {e}")
322
+
323
+ async def integrate(self, hass_code: str) -> Dict[str, Any]:
324
+ """Integrate with Home Assistant using hassCode.
325
+
326
+ Args:
327
+ hass_code: Home Assistant integration code from APP
328
+
329
+ Returns:
330
+ Integration result dictionary
331
+
332
+ Raises:
333
+ APIError: If API request fails
334
+ """
335
+ # 根据 hass_code 动态更新 base_url(如果之前没有明确指定)
336
+ # 如果 hass_code 以 "db-" 开头,切换到正式环境
337
+ if hass_code.startswith("db-") and self.base_url != self.PROD_BASE_URL:
338
+ old_url = self.base_url
339
+ self.base_url = self.PROD_BASE_URL
340
+ _LOGGER.info(
341
+ "Switching to production environment based on hass_code. "
342
+ "URL changed from %s to %s", old_url, self.base_url
343
+ )
344
+ elif not hass_code.startswith("db-") and self.base_url != self.TEST_BASE_URL:
345
+ # 如果 hass_code 不以 "db-" 开头,且当前不是测试环境,切换到测试环境
346
+ old_url = self.base_url
347
+ self.base_url = self.TEST_BASE_URL
348
+ _LOGGER.info(
349
+ "Switching to test environment based on hass_code. "
350
+ "URL changed from %s to %s", old_url, self.base_url
351
+ )
352
+
353
+ try:
354
+ session = self._get_session()
355
+ url = f"{self.base_url}hass/integrate"
356
+ payload = {"hassCode": hass_code}
357
+
358
+ async with session.post(url, json=payload) as resp:
359
+ if resp.status == 200:
360
+ data = await resp.json()
361
+ _LOGGER.debug("Integration successful: %s", data)
362
+ return data
363
+ else:
364
+ error_text = await resp.text()
365
+ _LOGGER.error("Failed to integrate: %s", error_text)
366
+ raise APIError(f"API error {resp.status}: {error_text}")
367
+ except aiohttp.ClientError as e:
368
+ _LOGGER.exception("Client error while integrating: %s", e)
369
+ raise APIError(f"Client error: {e}")
370
+ except Exception as e:
371
+ _LOGGER.exception("Exception while integrating: %s", e)
372
+ raise DayBetterError(f"Unexpected error: {e}")
373
+
374
+ @property
375
+ def is_authenticated(self) -> bool:
376
+ """Check if the API client is authenticated."""
377
+ return self._auth_valid
378
+
379
+ def filter_sensor_devices(
380
+ self,
381
+ devices: List[Dict[str, Any]],
382
+ pids: Dict[str, Any],
383
+ ) -> List[Dict[str, Any]]:
384
+ """Filter devices to only include sensors based on PID.
385
+
386
+ Args:
387
+ devices: List of all devices
388
+ pids: Dictionary containing device type PIDs
389
+
390
+ Returns:
391
+ List of sensor devices only
392
+ """
393
+ sensor_pids_str = pids.get("sensor", "")
394
+ if not sensor_pids_str:
395
+ return []
396
+
397
+ sensor_pids = {pid.strip() for pid in sensor_pids_str.split(",")}
398
+
399
+ return [
400
+ device
401
+ for device in devices
402
+ if device.get("deviceMoldPid", "") in sensor_pids
403
+ ]
404
+
405
+ def merge_device_status(
406
+ self,
407
+ devices: List[Dict[str, Any]],
408
+ statuses: List[Dict[str, Any]],
409
+ ) -> List[Dict[str, Any]]:
410
+ """Merge device info with status info.
411
+
412
+ Args:
413
+ devices: List of device info dictionaries
414
+ statuses: List of device status dictionaries
415
+
416
+ Returns:
417
+ List of merged device dictionaries
418
+ """
419
+ status_dict = {status.get("deviceName"): status for status in statuses}
420
+
421
+ merged = []
422
+ for device in devices:
423
+ device_name = device.get("deviceName")
424
+ merged_device = device.copy()
425
+
426
+ if device_name in status_dict:
427
+ merged_device.update(status_dict[device_name])
428
+
429
+ merged.append(merged_device)
430
+
431
+ return merged
432
+
433
+ async def fetch_sensor_data(self) -> List[Dict[str, Any]]:
434
+ """Fetch and process sensor data in one call.
435
+
436
+ This method fetches device statuses, devices list, and PIDs,
437
+ filters for sensor devices, and merges the data.
438
+
439
+ Returns:
440
+ List of sensor devices with merged status data
441
+
442
+ Raises:
443
+ AuthenticationError: If authentication fails
444
+ APIError: If API request fails
445
+ """
446
+ # Fetch current statuses
447
+ statuses = await self.fetch_device_statuses()
448
+
449
+ # Fetch devices and PIDs if not cached
450
+ if not self._devices or not self._pids:
451
+ self._devices = await self.fetch_devices()
452
+ self._pids = await self.fetch_pids()
453
+
454
+ # Filter to sensor devices only
455
+ sensor_devices = self.filter_sensor_devices(self._devices, self._pids)
456
+
457
+ # Merge with current status
458
+ merged = self.merge_device_status(sensor_devices, statuses)
459
+ _LOGGER.debug("Fetched %d sensor devices", len(merged))
460
+ return merged
461
+
462
+ async def close(self) -> None:
463
+ """Close the client session."""
464
+ if self._session:
465
+ await self._session.close()
466
+ self._session = None
@@ -0,0 +1,38 @@
1
+ """DayBetter client exceptions."""
2
+
3
+
4
+ class DayBetterError(Exception):
5
+ """Base exception for DayBetter client."""
6
+
7
+ def __init__(self, message: str) -> None:
8
+ """Initialize the exception.
9
+
10
+ Args:
11
+ message: Error message
12
+ """
13
+ super().__init__(message)
14
+ self.message = message
15
+
16
+
17
+ class AuthenticationError(DayBetterError):
18
+ """Authentication failed."""
19
+
20
+ def __init__(self, message: str = "Authentication failed") -> None:
21
+ """Initialize the authentication error.
22
+
23
+ Args:
24
+ message: Error message
25
+ """
26
+ super().__init__(message)
27
+
28
+
29
+ class APIError(DayBetterError):
30
+ """API request failed."""
31
+
32
+ def __init__(self, message: str) -> None:
33
+ """Initialize the API error.
34
+
35
+ Args:
36
+ message: Error message
37
+ """
38
+ super().__init__(message)
File without changes
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: daybetter-services-python
3
+ Version: 1.0.7
4
+ Summary: Python client for DayBetter devices and services
5
+ Home-page: https://github.com/THDayBetter/daybetter-python
6
+ Author: THDayBetter
7
+ Author-email: THDayBetter <chenp2368@163.com>
8
+ Maintainer-email: THDayBetter <chenp2368@163.com>
9
+ License-Expression: MIT
10
+ Project-URL: Homepage, https://github.com/THDayBetter/daybetter-python
11
+ Project-URL: Documentation, https://github.com/THDayBetter/daybetter-python#readme
12
+ Project-URL: Repository, https://github.com/THDayBetter/daybetter-python.git
13
+ Project-URL: Bug Tracker, https://github.com/THDayBetter/daybetter-python/issues
14
+ Keywords: daybetter,iot,home automation,mqtt
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.8
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: aiohttp>=3.8.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=6.0; extra == "dev"
31
+ Requires-Dist: pytest-asyncio; extra == "dev"
32
+ Requires-Dist: black; extra == "dev"
33
+ Requires-Dist: isort; extra == "dev"
34
+ Requires-Dist: flake8; extra == "dev"
35
+ Requires-Dist: mypy; extra == "dev"
36
+ Dynamic: author
37
+ Dynamic: home-page
38
+ Dynamic: license-file
39
+ Dynamic: requires-python
40
+
41
+ # DayBetter Python Client
42
+
43
+ A Python client library for interacting with DayBetter devices and services.
44
+
45
+ ## Features
46
+
47
+ - Device management and control
48
+ - MQTT configuration retrieval
49
+ - Authentication handling
50
+ - Async/await support
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ pip install daybetter-services-python
56
+ ```
57
+
58
+ ## Usage
59
+
60
+ ```python
61
+ import asyncio
62
+ from daybetter_python import DayBetterClient
63
+
64
+ async def main():
65
+ async with DayBetterClient(token="your_token") as client:
66
+ # Fetch devices
67
+ devices = await client.fetch_devices()
68
+ print(f"Found {len(devices)} devices")
69
+
70
+ # Control a device
71
+ result = await client.control_device(
72
+ device_name="device_001",
73
+ action=True,
74
+ brightness=80
75
+ )
76
+ print(f"Control result: {result}")
77
+
78
+ if __name__ == "__main__":
79
+ asyncio.run(main())
80
+ ```
81
+
82
+ ## API Reference
83
+
84
+ ### DayBetterClient
85
+
86
+ #### Methods
87
+
88
+ - `fetch_devices()`: Get list of devices
89
+ - `fetch_pids()`: Get device type PIDs
90
+ - `control_device(device_name, action, brightness, hs_color, color_temp)`: Control a device
91
+ - `fetch_mqtt_config()`: Get MQTT configuration
92
+
93
+ ## License
94
+
95
+ MIT License
96
+
97
+ ## Contributing
98
+
99
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,9 @@
1
+ daybetter_python/__init__.py,sha256=xLxgdbAff9zysqqwgS3FQ8Ba0b4tOi0pKWyjF6MyTO8,252
2
+ daybetter_python/client.py,sha256=ayAHorsnvGeQ1PWGRP-OD9WK0OP6DHQ6rxcq0h3_BMw,18009
3
+ daybetter_python/exceptions.py,sha256=MGZdRDe711BOCF2bE8Oc7Gph-QXx7HbszOUsbPYuUjQ,903
4
+ daybetter_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ daybetter_services_python-1.0.7.dist-info/licenses/LICENSE,sha256=DbDD4ZqrGs-Zj3ffqcf35iwo9gM2bBc4WSCYbohM1-M,1068
6
+ daybetter_services_python-1.0.7.dist-info/METADATA,sha256=0HDBAzHqnrnUy_xeOPPhGMBOr9mR6EiS8CinO8Uhnio,2845
7
+ daybetter_services_python-1.0.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ daybetter_services_python-1.0.7.dist-info/top_level.txt,sha256=cRqnqd_T8N2nPkR6x6fqB-DkUgXQ0baNqayVn6RdDFo,17
9
+ daybetter_services_python-1.0.7.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) 2024 THDayBetter
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
+ daybetter_python