lmnr 0.4.48__py3-none-any.whl → 0.4.50__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.
lmnr/cli.py CHANGED
@@ -1,40 +1,74 @@
1
1
  from argparse import ArgumentParser
2
2
  import asyncio
3
3
  import importlib.util
4
+ import logging
4
5
  import os
6
+ import re
5
7
  import sys
6
8
 
7
9
  from .sdk.eval_control import PREPARE_ONLY, EVALUATION_INSTANCE
10
+ from .sdk.log import ColorfulFormatter
11
+
12
+ LOG = logging.getLogger(__name__)
13
+ console_log_handler = logging.StreamHandler()
14
+ console_log_handler.setFormatter(ColorfulFormatter())
15
+ LOG.addHandler(console_log_handler)
16
+
17
+
18
+ EVAL_DIR = "evals"
8
19
 
9
20
 
10
21
  async def run_evaluation(args):
11
22
  sys.path.append(os.getcwd())
12
23
 
13
- prep_token = PREPARE_ONLY.set(True)
14
- try:
15
- file = os.path.abspath(args.file)
16
- name = "user_module"
24
+ if args.file is None:
25
+ files = [
26
+ os.path.join(EVAL_DIR, f)
27
+ for f in os.listdir(EVAL_DIR)
28
+ if re.match(r".*_eval\.py$", f) or re.match(r"eval_.*\.py$", f)
29
+ ]
30
+ if len(files) == 0:
31
+ LOG.error("No evaluation files found in evals directory")
32
+ return
33
+ files.sort()
34
+ LOG.info(f"Located {len(files)} evaluation files in {EVAL_DIR}")
35
+
36
+ else:
37
+ files = [args.file]
17
38
 
18
- spec = importlib.util.spec_from_file_location(name, file)
19
- if spec is None or spec.loader is None:
20
- raise ImportError(f"Could not load module specification from {file}")
21
- mod = importlib.util.module_from_spec(spec)
22
- sys.modules[name] = mod
39
+ for file in files:
40
+ prep_token = PREPARE_ONLY.set(True)
41
+ LOG.info(f"Running evaluation from {file}")
42
+ try:
43
+ file = os.path.abspath(file)
44
+ name = "user_module" + file
23
45
 
24
- spec.loader.exec_module(mod)
25
- evaluation = EVALUATION_INSTANCE.get()
26
- if evaluation is None:
27
- raise RuntimeError("Evaluation instance not found")
46
+ spec = importlib.util.spec_from_file_location(name, file)
47
+ if spec is None or spec.loader is None:
48
+ LOG.error(f"Could not load module specification from {file}")
49
+ if args.fail_on_error:
50
+ return
51
+ continue
52
+ mod = importlib.util.module_from_spec(spec)
53
+ sys.modules[name] = mod
28
54
 
29
- await evaluation.run()
30
- finally:
31
- PREPARE_ONLY.reset(prep_token)
55
+ spec.loader.exec_module(mod)
56
+ evaluation = EVALUATION_INSTANCE.get()
57
+ if evaluation is None:
58
+ LOG.warning("Evaluation instance not found")
59
+ if args.fail_on_error:
60
+ return
61
+ continue
62
+
63
+ await evaluation.run()
64
+ finally:
65
+ PREPARE_ONLY.reset(prep_token)
32
66
 
33
67
 
34
68
  def cli():
35
69
  parser = ArgumentParser(
36
70
  prog="lmnr",
37
- description="CLI for Laminar",
71
+ description="CLI for Laminar. Call `lmnr [subcommand] --help` for more information on each subcommand.",
38
72
  )
39
73
 
40
74
  subparsers = parser.add_subparsers(title="subcommands", dest="subcommand")
@@ -44,7 +78,21 @@ def cli():
44
78
  description="Run an evaluation",
45
79
  help="Run an evaluation",
46
80
  )
47
- parser_eval.add_argument("file", help="A file containing the evaluation to run")
81
+ parser_eval.add_argument(
82
+ "file",
83
+ nargs="?",
84
+ help="A file containing the evaluation to run."
85
+ + "If no file name is provided, all evaluation files in the `evals` directory are run as long"
86
+ + "as they match *_eval.py or eval_*.py",
87
+ default=None,
88
+ )
89
+
90
+ parser_eval.add_argument(
91
+ "--fail-on-error",
92
+ action="store_true",
93
+ default=False,
94
+ help="Fail on error",
95
+ )
48
96
 
49
97
  parsed = parser.parse_args()
50
98
  if parsed.subcommand == "eval":
@@ -509,7 +509,7 @@ def init_chroma_instrumentor():
509
509
 
510
510
  def init_google_generativeai_instrumentor():
511
511
  try:
512
- if is_package_installed("google.generativeai") and is_package_installed(
512
+ if is_package_installed("google-generativeai") and is_package_installed(
513
513
  "opentelemetry-instrumentation-google-generativeai"
514
514
  ):
515
515
  from opentelemetry.instrumentation.google_generativeai import (
lmnr/sdk/evaluations.py CHANGED
@@ -29,8 +29,17 @@ from .utils import is_async
29
29
  DEFAULT_BATCH_SIZE = 5
30
30
 
31
31
 
32
- def get_evaluation_url(project_id: str, evaluation_id: str):
33
- return f"https://www.lmnr.ai/project/{project_id}/evaluations/{evaluation_id}"
32
+ def get_evaluation_url(
33
+ project_id: str, evaluation_id: str, base_url: str = "https://www.lmnr.ai"
34
+ ):
35
+ url = base_url
36
+ if url.endswith("/"):
37
+ url = url[:-1]
38
+ if url.endswith("localhost") or url.endswith("127.0.0.1"):
39
+ # We best effort assume that the frontend is running on port 3000
40
+ # TODO: expose the frontend port?
41
+ url = url + ":3000"
42
+ return f"{url}/project/{project_id}/evaluations/{evaluation_id}"
34
43
 
35
44
 
36
45
  def get_average_scores(results: list[EvaluationResultDatapoint]) -> dict[str, Numeric]:
@@ -49,8 +58,8 @@ def get_average_scores(results: list[EvaluationResultDatapoint]) -> dict[str, Nu
49
58
 
50
59
 
51
60
  class EvaluationReporter:
52
- def __init__(self):
53
- pass
61
+ def __init__(self, base_url: str = "https://www.lmnr.ai"):
62
+ self.base_url = base_url
54
63
 
55
64
  def start(self, length: int):
56
65
  self.cli_progress = tqdm(
@@ -71,7 +80,7 @@ class EvaluationReporter:
71
80
  ):
72
81
  self.cli_progress.close()
73
82
  print(
74
- f"\nCheck the results at {get_evaluation_url(project_id, evaluation_id)}\n"
83
+ f"\nCheck the results at {get_evaluation_url(project_id, evaluation_id, self.base_url)}\n"
75
84
  )
76
85
  print("Average scores:")
77
86
  for name, score in average_scores.items():
@@ -160,7 +169,7 @@ class Evaluation:
160
169
  )
161
170
 
162
171
  self.is_finished = False
163
- self.reporter = EvaluationReporter()
172
+ self.reporter = EvaluationReporter(base_url)
164
173
  if isinstance(data, list):
165
174
  self.data = [
166
175
  (Datapoint.model_validate(point) if isinstance(point, dict) else point)
lmnr/sdk/laminar.py CHANGED
@@ -136,15 +136,7 @@ class Laminar:
136
136
  cls.__initialized = True
137
137
  cls._initialize_logger()
138
138
 
139
- if _processor is not None:
140
- cls.__logger.warning(
141
- "Using a custom span processor. This feature is added for tests only. "
142
- "Any use of this feature outside of tests is not supported and "
143
- "advised against."
144
- )
145
-
146
139
  Traceloop.init(
147
- processor=_processor,
148
140
  exporter=OTLPSpanExporter(
149
141
  endpoint=cls.__base_grpc_url,
150
142
  headers={"authorization": f"Bearer {cls.__project_api_key}"},
@@ -407,7 +399,7 @@ class Laminar:
407
399
  )
408
400
  yield span
409
401
 
410
- # # TODO: Figure out if this is necessary
402
+ # TODO: Figure out if this is necessary
411
403
  try:
412
404
  detach(ctx_token)
413
405
  except Exception:
lmnr/sdk/log.py CHANGED
@@ -24,6 +24,29 @@ class CustomFormatter(logging.Formatter):
24
24
  return formatter.format(record)
25
25
 
26
26
 
27
+ class ColorfulFormatter(logging.Formatter):
28
+ grey = "\x1b[38;20m"
29
+ green = "\x1b[32;20m"
30
+ yellow = "\x1b[33;20m"
31
+ red = "\x1b[31;20m"
32
+ bold_red = "\x1b[31;1m"
33
+ reset = "\x1b[0m"
34
+ fmt = "Laminar %(levelname)s: %(message)s"
35
+
36
+ FORMATS = {
37
+ logging.DEBUG: grey + fmt + reset,
38
+ logging.INFO: green + fmt + reset,
39
+ logging.WARNING: yellow + fmt + reset,
40
+ logging.ERROR: red + fmt + reset,
41
+ logging.CRITICAL: bold_red + fmt + reset,
42
+ }
43
+
44
+ def format(self, record: logging.LogRecord):
45
+ log_fmt = self.FORMATS.get(record.levelno)
46
+ formatter = logging.Formatter(log_fmt)
47
+ return formatter.format(record)
48
+
49
+
27
50
  # For StreamHandlers / console
28
51
  class VerboseColorfulFormatter(CustomFormatter):
29
52
  def format(self, record):
@@ -32,7 +55,7 @@ class VerboseColorfulFormatter(CustomFormatter):
32
55
 
33
56
  # For Verbose FileHandlers / files
34
57
  class VerboseFormatter(CustomFormatter):
35
- fmt = "%(asctime)s::%(name)s::%(levelname)s| %(message)s (%(filename)s:%(lineno)d)"
58
+ fmt = "%(asctime)s::%(name)s::%(levelname)s: %(message)s (%(filename)s:%(lineno)d)"
36
59
 
37
60
  def format(self, record):
38
61
  formatter = logging.Formatter(self.fmt)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lmnr
3
- Version: 0.4.48
3
+ Version: 0.4.50
4
4
  Summary: Python SDK for Laminar AI
5
5
  License: Apache-2.0
6
6
  Author: lmnr.ai
@@ -44,35 +44,35 @@ Requires-Dist: deprecated (>=1.0)
44
44
  Requires-Dist: opentelemetry-api (>=1.28.0)
45
45
  Requires-Dist: opentelemetry-exporter-otlp-proto-grpc (>=1.28.0)
46
46
  Requires-Dist: opentelemetry-exporter-otlp-proto-http (>=1.28.0)
47
- Requires-Dist: opentelemetry-instrumentation-alephalpha (>=0.33.12) ; extra == "all" or extra == "alephalpha"
48
- Requires-Dist: opentelemetry-instrumentation-anthropic (>=0.33.12) ; extra == "all" or extra == "anthropic"
49
- Requires-Dist: opentelemetry-instrumentation-bedrock (>=0.33.12) ; extra == "all" or extra == "bedrock"
50
- Requires-Dist: opentelemetry-instrumentation-chromadb (>=0.33.12) ; extra == "all" or extra == "chromadb"
51
- Requires-Dist: opentelemetry-instrumentation-cohere (>=0.33.12) ; extra == "all" or extra == "cohere"
52
- Requires-Dist: opentelemetry-instrumentation-google-generativeai (>=0.33.12) ; extra == "all" or extra == "google-generativeai"
53
- Requires-Dist: opentelemetry-instrumentation-groq (>=0.33.12) ; extra == "all" or extra == "groq"
54
- Requires-Dist: opentelemetry-instrumentation-haystack (>=0.33.12) ; extra == "all" or extra == "haystack"
55
- Requires-Dist: opentelemetry-instrumentation-lancedb (>=0.33.12) ; extra == "all" or extra == "lancedb"
56
- Requires-Dist: opentelemetry-instrumentation-langchain (>=0.33.12) ; extra == "all" or extra == "langchain"
57
- Requires-Dist: opentelemetry-instrumentation-llamaindex (>=0.33.12) ; extra == "all" or extra == "llamaindex"
58
- Requires-Dist: opentelemetry-instrumentation-marqo (>=0.33.12) ; extra == "all" or extra == "marqo"
59
- Requires-Dist: opentelemetry-instrumentation-milvus (>=0.33.12) ; extra == "all" or extra == "milvus"
60
- Requires-Dist: opentelemetry-instrumentation-mistralai (>=0.33.12) ; extra == "all" or extra == "mistralai"
61
- Requires-Dist: opentelemetry-instrumentation-ollama (>=0.33.12) ; extra == "all" or extra == "ollama"
47
+ Requires-Dist: opentelemetry-instrumentation-alephalpha (>=0.34.0) ; extra == "all" or extra == "alephalpha"
48
+ Requires-Dist: opentelemetry-instrumentation-anthropic (>=0.34.0) ; extra == "all" or extra == "anthropic"
49
+ Requires-Dist: opentelemetry-instrumentation-bedrock (>=0.34.0) ; extra == "all" or extra == "bedrock"
50
+ Requires-Dist: opentelemetry-instrumentation-chromadb (>=0.34.0) ; extra == "all" or extra == "chromadb"
51
+ Requires-Dist: opentelemetry-instrumentation-cohere (>=0.34.0) ; extra == "all" or extra == "cohere"
52
+ Requires-Dist: opentelemetry-instrumentation-google-generativeai (>=0.34.0) ; extra == "all" or extra == "google-generativeai"
53
+ Requires-Dist: opentelemetry-instrumentation-groq (>=0.34.0) ; extra == "all" or extra == "groq"
54
+ Requires-Dist: opentelemetry-instrumentation-haystack (>=0.34.0) ; extra == "all" or extra == "haystack"
55
+ Requires-Dist: opentelemetry-instrumentation-lancedb (>=0.34.0) ; extra == "all" or extra == "lancedb"
56
+ Requires-Dist: opentelemetry-instrumentation-langchain (>=0.34.0) ; extra == "all" or extra == "langchain"
57
+ Requires-Dist: opentelemetry-instrumentation-llamaindex (>=0.34.0) ; extra == "all" or extra == "llamaindex"
58
+ Requires-Dist: opentelemetry-instrumentation-marqo (>=0.34.0) ; extra == "all" or extra == "marqo"
59
+ Requires-Dist: opentelemetry-instrumentation-milvus (>=0.34.0) ; extra == "all" or extra == "milvus"
60
+ Requires-Dist: opentelemetry-instrumentation-mistralai (>=0.34.0) ; extra == "all" or extra == "mistralai"
61
+ Requires-Dist: opentelemetry-instrumentation-ollama (>=0.34.0) ; extra == "all" or extra == "ollama"
62
62
  Requires-Dist: opentelemetry-instrumentation-openai (>=0.33.12) ; extra == "all" or extra == "openai"
63
- Requires-Dist: opentelemetry-instrumentation-pinecone (>=0.33.12) ; extra == "all" or extra == "pinecone"
64
- Requires-Dist: opentelemetry-instrumentation-qdrant (>=0.33.12) ; extra == "all" or extra == "qdrant"
65
- Requires-Dist: opentelemetry-instrumentation-replicate (>=0.33.12) ; extra == "all" or extra == "replicate"
66
- Requires-Dist: opentelemetry-instrumentation-requests (>=0.49b0,<0.50)
67
- Requires-Dist: opentelemetry-instrumentation-sagemaker (>=0.33.12) ; extra == "all" or extra == "sagemaker"
68
- Requires-Dist: opentelemetry-instrumentation-sqlalchemy (>=0.49b0,<0.50)
69
- Requires-Dist: opentelemetry-instrumentation-threading (>=0.49b0,<0.50)
70
- Requires-Dist: opentelemetry-instrumentation-together (>=0.33.12) ; extra == "all" or extra == "together"
71
- Requires-Dist: opentelemetry-instrumentation-transformers (>=0.33.12) ; extra == "all" or extra == "transformers"
72
- Requires-Dist: opentelemetry-instrumentation-urllib3 (>=0.49b0,<0.50)
73
- Requires-Dist: opentelemetry-instrumentation-vertexai (>=0.33.12) ; extra == "all" or extra == "vertexai"
74
- Requires-Dist: opentelemetry-instrumentation-watsonx (>=0.33.12) ; extra == "all" or extra == "watsonx"
75
- Requires-Dist: opentelemetry-instrumentation-weaviate (>=0.33.12) ; extra == "all" or extra == "weaviate"
63
+ Requires-Dist: opentelemetry-instrumentation-pinecone (>=0.34.0) ; extra == "all" or extra == "pinecone"
64
+ Requires-Dist: opentelemetry-instrumentation-qdrant (>=0.34.0) ; extra == "all" or extra == "qdrant"
65
+ Requires-Dist: opentelemetry-instrumentation-replicate (>=0.34.0) ; extra == "all" or extra == "replicate"
66
+ Requires-Dist: opentelemetry-instrumentation-requests (>=0.50b0)
67
+ Requires-Dist: opentelemetry-instrumentation-sagemaker (>=0.34.0) ; extra == "all" or extra == "sagemaker"
68
+ Requires-Dist: opentelemetry-instrumentation-sqlalchemy (>=0.50b0)
69
+ Requires-Dist: opentelemetry-instrumentation-threading (>=0.50b0)
70
+ Requires-Dist: opentelemetry-instrumentation-together (>=0.34.0) ; extra == "all" or extra == "together"
71
+ Requires-Dist: opentelemetry-instrumentation-transformers (>=0.34.0) ; extra == "all" or extra == "transformers"
72
+ Requires-Dist: opentelemetry-instrumentation-urllib3 (>=0.50b0)
73
+ Requires-Dist: opentelemetry-instrumentation-vertexai (>=0.34.0) ; extra == "all" or extra == "vertexai"
74
+ Requires-Dist: opentelemetry-instrumentation-watsonx (>=0.34.0) ; extra == "all" or extra == "watsonx"
75
+ Requires-Dist: opentelemetry-instrumentation-weaviate (>=0.34.0) ; extra == "all" or extra == "weaviate"
76
76
  Requires-Dist: opentelemetry-sdk (>=1.28.0)
77
77
  Requires-Dist: opentelemetry-semantic-conventions-ai (==0.4.2)
78
78
  Requires-Dist: pydantic (>=2.7)
@@ -1,5 +1,5 @@
1
1
  lmnr/__init__.py,sha256=Bqxs-8Mh4h69pOHURgBCgo9EW1GwChebxP6wUX2-bsU,452
2
- lmnr/cli.py,sha256=W5zn5DBJiFaVARFy2D-8tJI8ZlGNzjJqk3qiXAG2woo,1456
2
+ lmnr/cli.py,sha256=4J2RZQhHM3jJcjFvBC4PChQTS-ukxykVvI0X6lTkK-o,2918
3
3
  lmnr/openllmetry_sdk/.flake8,sha256=bCxuDlGx3YQ55QHKPiGJkncHanh9qGjQJUujcFa3lAU,150
4
4
  lmnr/openllmetry_sdk/.python-version,sha256=9OLQBQVbD4zE4cJsPePhnAfV_snrPSoqEQw-PXgPMOs,6
5
5
  lmnr/openllmetry_sdk/__init__.py,sha256=vVSGTAwUnJvdulHtslkGAd8QCBuv78WUK3bgfBpH6Do,2390
@@ -11,7 +11,7 @@ lmnr/openllmetry_sdk/tracing/__init__.py,sha256=xT73L1t2si2CM6QmMiTZ7zn-dKKYBLNr
11
11
  lmnr/openllmetry_sdk/tracing/attributes.py,sha256=B_4KVYWAUu-6DQmsm2eCJQcTxm8pG1EByCBK3uOPkuI,1293
12
12
  lmnr/openllmetry_sdk/tracing/content_allow_list.py,sha256=3feztm6PBWNelc8pAZUcQyEGyeSpNiVKjOaDk65l2ps,846
13
13
  lmnr/openllmetry_sdk/tracing/context_manager.py,sha256=rdSus-p-TaevQ8hIAhfbnZr5dTqRvACDkzXGDpflncY,306
14
- lmnr/openllmetry_sdk/tracing/tracing.py,sha256=pa8ZPhH4NSwP1QnvD5qNaOZ775H5Q4KVa9OwSki9KUE,32138
14
+ lmnr/openllmetry_sdk/tracing/tracing.py,sha256=X87Dw-WhtGP-THeiTRzZ1FBosRdY2FR1xEcE09PgkRI,32138
15
15
  lmnr/openllmetry_sdk/utils/__init__.py,sha256=pNhf0G3vTd5ccoc03i1MXDbricSaiqCbi1DLWhSekK8,604
16
16
  lmnr/openllmetry_sdk/utils/in_memory_span_exporter.py,sha256=H_4TRaThMO1H6vUQ0OpQvzJk_fZH0OOsRAM1iZQXsR8,2112
17
17
  lmnr/openllmetry_sdk/utils/json_encoder.py,sha256=dK6b_axr70IYL7Vv-bu4wntvDDuyntoqsHaddqX7P58,463
@@ -21,13 +21,13 @@ lmnr/sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  lmnr/sdk/datasets.py,sha256=KNMp_v3z1ocIltIw7kTgj8o-l9R8N8Tgj0sw1ajQ9C8,1582
22
22
  lmnr/sdk/decorators.py,sha256=ja2EUWUWvFOp28ER0k78PRuxNahwCVyH0TdM3U-xY7U,1856
23
23
  lmnr/sdk/eval_control.py,sha256=G6Fg3Xx_KWv72iBaWlNMdyRTF2bZFQnwJ68sJNSpIcY,177
24
- lmnr/sdk/evaluations.py,sha256=gLImD_uB9uXgw07QiJ_OYRTFDGxiPtFCO1c8HyOq2s0,15935
25
- lmnr/sdk/laminar.py,sha256=pgvkCswo9csOIWD3DWr6EW2UqMbUWc-AomVVOGD2U2E,31418
26
- lmnr/sdk/log.py,sha256=cZBeUoSK39LMEV-X4-eEhTWOciULRfHaKfRK8YqIM8I,1532
24
+ lmnr/sdk/evaluations.py,sha256=lSXSvvYNB5JsSGli_wV-W34LVzJkQOiK8DnvHv4eU5A,16323
25
+ lmnr/sdk/laminar.py,sha256=sUq04bSGfrNuSHOEMTbR01Me0ploX8SE5GIWKqUAVyY,31094
26
+ lmnr/sdk/log.py,sha256=nt_YMmPw1IRbGy0b7q4rTtP4Yo3pQfNxqJPXK3nDSNQ,2213
27
27
  lmnr/sdk/types.py,sha256=FCNoFoa0ingOvpXGfbiETVsakYyq9Zpoc56MXJ1YDzQ,6390
28
28
  lmnr/sdk/utils.py,sha256=Uk8y15x-sd5tP2ERONahElLDJVEy_3dA_1_5g9A6auY,3358
29
- lmnr-0.4.48.dist-info/LICENSE,sha256=67b_wJHVV1CBaWkrKFWU1wyqTPSdzH77Ls-59631COg,10411
30
- lmnr-0.4.48.dist-info/METADATA,sha256=KCZg8NS72mThs3tAJ4SL9OwDSTcvsosx5JxzpDTtVTw,12244
31
- lmnr-0.4.48.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
32
- lmnr-0.4.48.dist-info/entry_points.txt,sha256=K1jE20ww4jzHNZLnsfWBvU3YKDGBgbOiYG5Y7ivQcq4,37
33
- lmnr-0.4.48.dist-info/RECORD,,
29
+ lmnr-0.4.50.dist-info/LICENSE,sha256=67b_wJHVV1CBaWkrKFWU1wyqTPSdzH77Ls-59631COg,10411
30
+ lmnr-0.4.50.dist-info/METADATA,sha256=Qpu8olLKxcpmaPPf1aczP16sRM_4KyHwkueRLp56EKM,12196
31
+ lmnr-0.4.50.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
32
+ lmnr-0.4.50.dist-info/entry_points.txt,sha256=K1jE20ww4jzHNZLnsfWBvU3YKDGBgbOiYG5Y7ivQcq4,37
33
+ lmnr-0.4.50.dist-info/RECORD,,
File without changes
File without changes