expr_tracker 0.1.2__tar.gz → 0.1.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: expr_tracker
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Add your description here
5
5
  Author-email: HSPK <whxway@whu.edu.cn>
6
6
  Requires-Python: >=3.10
@@ -12,6 +12,9 @@ dependencies = [
12
12
  ]
13
13
  dynamic = ["version"]
14
14
 
15
+ [project.scripts]
16
+ et = "expr_tracker.cli:main"
17
+
15
18
  [dependency-groups]
16
19
  dev = ["ipykernel>=6.30.1"]
17
20
 
@@ -0,0 +1,31 @@
1
+ from typing import Any, Type
2
+
3
+ from pydantic import BaseModel
4
+ from pydantic.version import VERSION as PYDANTIC_VERSION
5
+ from typing_extensions import Literal
6
+
7
+ PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2])
8
+ PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2
9
+
10
+ Url: Type[Any]
11
+
12
+ if PYDANTIC_V2:
13
+ from pydantic_core import PydanticUndefinedType
14
+ from pydantic_core import Url as Url
15
+
16
+ UndefinedType = PydanticUndefinedType
17
+
18
+ def _model_dump(
19
+ model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
20
+ ) -> Any:
21
+ return model.model_dump(mode=mode, **kwargs)
22
+ else:
23
+ from pydantic import AnyUrl as Url # noqa: F401
24
+ from pydantic.fields import ( # type: ignore[no-redef, attr-defined]
25
+ UndefinedType as UndefinedType, # noqa: F401
26
+ )
27
+
28
+ def _model_dump(
29
+ model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
30
+ ) -> Any:
31
+ return model.dict(**kwargs)
@@ -0,0 +1,15 @@
1
+ import click
2
+ import expr_tracker as et
3
+
4
+
5
+ @click.group()
6
+ def main():
7
+ pass
8
+
9
+
10
+ @main.command()
11
+ @click.argument("msg")
12
+ @click.option("--title", default="Alert", help="Title of the alert")
13
+ @click.option("--level", default="info", help="Level of the alert")
14
+ def alert(msg: str, title: str = "Alert", level: str = "info"):
15
+ et.alert(title=title, text=msg, level=level)
@@ -0,0 +1,344 @@
1
+ import dataclasses
2
+ import datetime
3
+ from collections import defaultdict, deque
4
+ from decimal import Decimal
5
+ from enum import Enum
6
+ from ipaddress import (
7
+ IPv4Address,
8
+ IPv4Interface,
9
+ IPv4Network,
10
+ IPv6Address,
11
+ IPv6Interface,
12
+ IPv6Network,
13
+ )
14
+ from pathlib import Path, PurePath
15
+ from re import Pattern
16
+ from types import GeneratorType
17
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
18
+ from uuid import UUID
19
+
20
+ from expr_tracker.types import IncEx
21
+ from pydantic import BaseModel
22
+ from pydantic.color import Color
23
+ from pydantic.networks import AnyUrl, NameEmail
24
+ from pydantic.types import SecretBytes, SecretStr
25
+ from typing_extensions import Annotated, Doc
26
+
27
+ from ._compat import PYDANTIC_V2, UndefinedType, Url, _model_dump
28
+
29
+
30
+ # Taken from Pydantic v1 as is
31
+ def isoformat(o: Union[datetime.date, datetime.time]) -> str:
32
+ return o.isoformat()
33
+
34
+
35
+ # Taken from Pydantic v1 as is
36
+ # TODO: pv2 should this return strings instead?
37
+ def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
38
+ """
39
+ Encodes a Decimal as int of there's no exponent, otherwise float
40
+
41
+ This is useful when we use ConstrainedDecimal to represent Numeric(x,0)
42
+ where a integer (but not int typed) is used. Encoding this as a float
43
+ results in failed round-tripping between encode and parse.
44
+ Our Id type is a prime example of this.
45
+
46
+ >>> decimal_encoder(Decimal("1.0"))
47
+ 1.0
48
+
49
+ >>> decimal_encoder(Decimal("1"))
50
+ 1
51
+ """
52
+ if dec_value.as_tuple().exponent >= 0: # type: ignore[operator]
53
+ return int(dec_value)
54
+ else:
55
+ return float(dec_value)
56
+
57
+
58
+ ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
59
+ bytes: lambda o: o.decode(),
60
+ Color: str,
61
+ datetime.date: isoformat,
62
+ datetime.datetime: isoformat,
63
+ datetime.time: isoformat,
64
+ datetime.timedelta: lambda td: td.total_seconds(),
65
+ Decimal: decimal_encoder,
66
+ Enum: lambda o: o.value,
67
+ frozenset: list,
68
+ deque: list,
69
+ GeneratorType: list,
70
+ IPv4Address: str,
71
+ IPv4Interface: str,
72
+ IPv4Network: str,
73
+ IPv6Address: str,
74
+ IPv6Interface: str,
75
+ IPv6Network: str,
76
+ NameEmail: str,
77
+ Path: str,
78
+ Pattern: lambda o: o.pattern,
79
+ SecretBytes: str,
80
+ SecretStr: str,
81
+ set: list,
82
+ UUID: str,
83
+ Url: str,
84
+ AnyUrl: str,
85
+ }
86
+
87
+
88
+ def generate_encoders_by_class_tuples(
89
+ type_encoder_map: Dict[Any, Callable[[Any], Any]],
90
+ ) -> Dict[Callable[[Any], Any], Tuple[Any, ...]]:
91
+ encoders_by_class_tuples: Dict[Callable[[Any], Any], Tuple[Any, ...]] = defaultdict(
92
+ tuple
93
+ )
94
+ for type_, encoder in type_encoder_map.items():
95
+ encoders_by_class_tuples[encoder] += (type_,)
96
+ return encoders_by_class_tuples
97
+
98
+
99
+ encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE)
100
+
101
+
102
+ def jsonable_encoder(
103
+ obj: Annotated[
104
+ Any,
105
+ Doc(
106
+ """
107
+ The input object to convert to JSON.
108
+ """
109
+ ),
110
+ ],
111
+ include: Annotated[
112
+ Optional[IncEx],
113
+ Doc(
114
+ """
115
+ Pydantic's `include` parameter, passed to Pydantic models to set the
116
+ fields to include.
117
+ """
118
+ ),
119
+ ] = None,
120
+ exclude: Annotated[
121
+ Optional[IncEx],
122
+ Doc(
123
+ """
124
+ Pydantic's `exclude` parameter, passed to Pydantic models to set the
125
+ fields to exclude.
126
+ """
127
+ ),
128
+ ] = None,
129
+ by_alias: Annotated[
130
+ bool,
131
+ Doc(
132
+ """
133
+ Pydantic's `by_alias` parameter, passed to Pydantic models to define if
134
+ the output should use the alias names (when provided) or the Python
135
+ attribute names. In an API, if you set an alias, it's probably because you
136
+ want to use it in the result, so you probably want to leave this set to
137
+ `True`.
138
+ """
139
+ ),
140
+ ] = True,
141
+ exclude_unset: Annotated[
142
+ bool,
143
+ Doc(
144
+ """
145
+ Pydantic's `exclude_unset` parameter, passed to Pydantic models to define
146
+ if it should exclude from the output the fields that were not explicitly
147
+ set (and that only had their default values).
148
+ """
149
+ ),
150
+ ] = False,
151
+ exclude_defaults: Annotated[
152
+ bool,
153
+ Doc(
154
+ """
155
+ Pydantic's `exclude_defaults` parameter, passed to Pydantic models to define
156
+ if it should exclude from the output the fields that had the same default
157
+ value, even when they were explicitly set.
158
+ """
159
+ ),
160
+ ] = False,
161
+ exclude_none: Annotated[
162
+ bool,
163
+ Doc(
164
+ """
165
+ Pydantic's `exclude_none` parameter, passed to Pydantic models to define
166
+ if it should exclude from the output any fields that have a `None` value.
167
+ """
168
+ ),
169
+ ] = False,
170
+ custom_encoder: Annotated[
171
+ Optional[Dict[Any, Callable[[Any], Any]]],
172
+ Doc(
173
+ """
174
+ Pydantic's `custom_encoder` parameter, passed to Pydantic models to define
175
+ a custom encoder.
176
+ """
177
+ ),
178
+ ] = None,
179
+ sqlalchemy_safe: Annotated[
180
+ bool,
181
+ Doc(
182
+ """
183
+ Exclude from the output any fields that start with the name `_sa`.
184
+
185
+ This is mainly a hack for compatibility with SQLAlchemy objects, they
186
+ store internal SQLAlchemy-specific state in attributes named with `_sa`,
187
+ and those objects can't (and shouldn't be) serialized to JSON.
188
+ """
189
+ ),
190
+ ] = True,
191
+ ) -> Any:
192
+ """
193
+ Convert any object to something that can be encoded in JSON.
194
+
195
+ This is used internally by FastAPI to make sure anything you return can be
196
+ encoded as JSON before it is sent to the client.
197
+
198
+ You can also use it yourself, for example to convert objects before saving them
199
+ in a database that supports only JSON.
200
+
201
+ Read more about it in the
202
+ [FastAPI docs for JSON Compatible Encoder](https://fastapi.tiangolo.com/tutorial/encoder/).
203
+ """
204
+ custom_encoder = custom_encoder or {}
205
+ if custom_encoder:
206
+ if type(obj) in custom_encoder:
207
+ return custom_encoder[type(obj)](obj)
208
+ else:
209
+ for encoder_type, encoder_instance in custom_encoder.items():
210
+ if isinstance(obj, encoder_type):
211
+ return encoder_instance(obj)
212
+ if include is not None and not isinstance(include, (set, dict)):
213
+ include = set(include)
214
+ if exclude is not None and not isinstance(exclude, (set, dict)):
215
+ exclude = set(exclude)
216
+ if isinstance(obj, BaseModel):
217
+ # TODO: remove when deprecating Pydantic v1
218
+ encoders: Dict[Any, Any] = {}
219
+ if not PYDANTIC_V2:
220
+ encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined]
221
+ if custom_encoder:
222
+ encoders = {**encoders, **custom_encoder}
223
+ obj_dict = _model_dump(
224
+ obj,
225
+ mode="json",
226
+ include=include,
227
+ exclude=exclude,
228
+ by_alias=by_alias,
229
+ exclude_unset=exclude_unset,
230
+ exclude_none=exclude_none,
231
+ exclude_defaults=exclude_defaults,
232
+ )
233
+ if "__root__" in obj_dict:
234
+ obj_dict = obj_dict["__root__"]
235
+ return jsonable_encoder(
236
+ obj_dict,
237
+ exclude_none=exclude_none,
238
+ exclude_defaults=exclude_defaults,
239
+ # TODO: remove when deprecating Pydantic v1
240
+ custom_encoder=encoders,
241
+ sqlalchemy_safe=sqlalchemy_safe,
242
+ )
243
+ if dataclasses.is_dataclass(obj):
244
+ assert not isinstance(obj, type)
245
+ obj_dict = dataclasses.asdict(obj)
246
+ return jsonable_encoder(
247
+ obj_dict,
248
+ include=include,
249
+ exclude=exclude,
250
+ by_alias=by_alias,
251
+ exclude_unset=exclude_unset,
252
+ exclude_defaults=exclude_defaults,
253
+ exclude_none=exclude_none,
254
+ custom_encoder=custom_encoder,
255
+ sqlalchemy_safe=sqlalchemy_safe,
256
+ )
257
+ if isinstance(obj, Enum):
258
+ return obj.value
259
+ if isinstance(obj, PurePath):
260
+ return str(obj)
261
+ if isinstance(obj, (str, int, float, type(None))):
262
+ return obj
263
+ if isinstance(obj, UndefinedType):
264
+ return None
265
+ if isinstance(obj, dict):
266
+ encoded_dict = {}
267
+ allowed_keys = set(obj.keys())
268
+ if include is not None:
269
+ allowed_keys &= set(include)
270
+ if exclude is not None:
271
+ allowed_keys -= set(exclude)
272
+ for key, value in obj.items():
273
+ if (
274
+ (
275
+ not sqlalchemy_safe
276
+ or (not isinstance(key, str))
277
+ or (not key.startswith("_sa"))
278
+ )
279
+ and (value is not None or not exclude_none)
280
+ and key in allowed_keys
281
+ ):
282
+ encoded_key = jsonable_encoder(
283
+ key,
284
+ by_alias=by_alias,
285
+ exclude_unset=exclude_unset,
286
+ exclude_none=exclude_none,
287
+ custom_encoder=custom_encoder,
288
+ sqlalchemy_safe=sqlalchemy_safe,
289
+ )
290
+ encoded_value = jsonable_encoder(
291
+ value,
292
+ by_alias=by_alias,
293
+ exclude_unset=exclude_unset,
294
+ exclude_none=exclude_none,
295
+ custom_encoder=custom_encoder,
296
+ sqlalchemy_safe=sqlalchemy_safe,
297
+ )
298
+ encoded_dict[encoded_key] = encoded_value
299
+ return encoded_dict
300
+ if isinstance(obj, (list, set, frozenset, GeneratorType, tuple, deque)):
301
+ encoded_list = []
302
+ for item in obj:
303
+ encoded_list.append(
304
+ jsonable_encoder(
305
+ item,
306
+ include=include,
307
+ exclude=exclude,
308
+ by_alias=by_alias,
309
+ exclude_unset=exclude_unset,
310
+ exclude_defaults=exclude_defaults,
311
+ exclude_none=exclude_none,
312
+ custom_encoder=custom_encoder,
313
+ sqlalchemy_safe=sqlalchemy_safe,
314
+ )
315
+ )
316
+ return encoded_list
317
+
318
+ if type(obj) in ENCODERS_BY_TYPE:
319
+ return ENCODERS_BY_TYPE[type(obj)](obj)
320
+ for encoder, classes_tuple in encoders_by_class_tuples.items():
321
+ if isinstance(obj, classes_tuple):
322
+ return encoder(obj)
323
+
324
+ try:
325
+ data = dict(obj)
326
+ except Exception as e:
327
+ errors: List[Exception] = []
328
+ errors.append(e)
329
+ try:
330
+ data = vars(obj)
331
+ except Exception as e:
332
+ errors.append(e)
333
+ raise ValueError(errors) from e
334
+ return jsonable_encoder(
335
+ data,
336
+ include=include,
337
+ exclude=exclude,
338
+ by_alias=by_alias,
339
+ exclude_unset=exclude_unset,
340
+ exclude_defaults=exclude_defaults,
341
+ exclude_none=exclude_none,
342
+ custom_encoder=custom_encoder,
343
+ sqlalchemy_safe=sqlalchemy_safe,
344
+ )
@@ -1,11 +1,6 @@
1
1
  from loguru import logger
2
2
  from pathlib import Path
3
-
4
-
5
- def custom_json_encoder(obj):
6
- if isinstance(obj, Path):
7
- return str(obj)
8
- raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")
3
+ from expr_tracker.encoders import jsonable_encoder
9
4
 
10
5
 
11
6
  class JsonlTracker:
@@ -38,7 +33,7 @@ class JsonlTracker:
38
33
  with open(self.config_fp, "w") as f:
39
34
  import json
40
35
 
41
- json.dump(config, f, indent=4, default=custom_json_encoder)
36
+ json.dump(jsonable_encoder(config), f, indent=4)
42
37
  self.print_to_screen = print_to_screen
43
38
  self.print_handle = print_handle
44
39
  self.current_step = (
@@ -0,0 +1,10 @@
1
+ import types
2
+ from enum import Enum
3
+ from typing import Any, Callable, Dict, Set, Type, TypeVar, Union
4
+
5
+ from pydantic import BaseModel
6
+
7
+ DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any])
8
+ UnionType = getattr(types, "UnionType", Union)
9
+ ModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str]
10
+ IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]]
@@ -288,7 +288,6 @@ wheels = [
288
288
 
289
289
  [[package]]
290
290
  name = "expr-tracker"
291
- version = "0.1.0"
292
291
  source = { editable = "." }
293
292
  dependencies = [
294
293
  { name = "jsonlines" },
@@ -306,7 +305,7 @@ dev = [
306
305
  requires-dist = [
307
306
  { name = "jsonlines", specifier = ">=4.0.0" },
308
307
  { name = "loguru", specifier = ">=0.7.3" },
309
- { name = "slark", specifier = ">=0.1.27" },
308
+ { name = "slark", specifier = ">=0.1.28" },
310
309
  { name = "wandb", specifier = ">=0.21.0" },
311
310
  ]
312
311
 
File without changes
File without changes