pipu-cli 0.1.dev3__py3-none-any.whl → 0.1.dev5__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.
pipu_cli/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  from .config import LOG_LEVEL
3
3
 
4
- __version__ = '0.1.dev3'
4
+ __version__ = '0.1.dev5'
5
5
 
6
6
 
7
7
  # Configure logging
pipu_cli/internals.py CHANGED
@@ -656,6 +656,9 @@ def update_packages_preserving_editable(
656
656
  2. Use the original source directory with pip install -e
657
657
  3. For regular packages, use normal pip install
658
658
 
659
+ When updating packages, constraints for those packages are temporarily excluded
660
+ to avoid conflicts between constraints and package dependencies.
661
+
659
662
  :param packages_to_update: List of package dictionaries with keys: name, latest_version, editable
660
663
  :param console: Optional Rich console for output
661
664
  :param timeout: Optional timeout for pip operations
@@ -664,6 +667,10 @@ def update_packages_preserving_editable(
664
667
  """
665
668
  import subprocess
666
669
  import sys
670
+ import tempfile
671
+ import os
672
+ from packaging.utils import canonicalize_name
673
+ from pathlib import Path
667
674
 
668
675
  if console is None:
669
676
  console = Console()
@@ -671,103 +678,142 @@ def update_packages_preserving_editable(
671
678
  successful_updates = []
672
679
  failed_updates = []
673
680
 
674
- # Helper function to run subprocess with cancellation support
675
- def run_with_cancel(cmd, timeout=None):
676
- """Run a subprocess command that can be cancelled."""
677
- process = subprocess.Popen(
678
- cmd,
679
- stdout=subprocess.PIPE,
680
- stderr=subprocess.PIPE,
681
- text=True
682
- )
681
+ # Get all current constraints and create a filtered version that excludes packages being updated
682
+ from .package_constraints import read_constraints
683
+ all_constraints = read_constraints()
683
684
 
684
- try:
685
- stdout, stderr = process.communicate(timeout=timeout)
686
- return process.returncode, stdout, stderr
687
- except subprocess.TimeoutExpired:
688
- process.kill()
689
- process.communicate() # Clean up
690
- raise
691
- except:
692
- # If we're interrupted or cancelled, kill the process
693
- if process.poll() is None: # Process still running
685
+ # Get canonical names of packages being updated
686
+ packages_being_updated = {canonicalize_name(pkg["name"]) for pkg in packages_to_update}
687
+
688
+ # Filter out constraints for packages being updated to avoid conflicts
689
+ filtered_constraints = {
690
+ pkg: constraint
691
+ for pkg, constraint in all_constraints.items()
692
+ if pkg not in packages_being_updated
693
+ }
694
+
695
+ # Create a temporary constraints file if there are any constraints to apply
696
+ constraint_file = None
697
+ constraint_file_path = None
698
+ try:
699
+ if filtered_constraints:
700
+ constraint_file = tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False)
701
+ constraint_file_path = constraint_file.name
702
+ for pkg, constraint in filtered_constraints.items():
703
+ constraint_file.write(f"{pkg}{constraint}\n")
704
+ constraint_file.close()
705
+ console.print(f"[dim]Using filtered constraints (excluding {len(packages_being_updated)} package(s) being updated)[/dim]")
706
+
707
+ # Helper function to run subprocess with cancellation support
708
+ def run_with_cancel(cmd, timeout=None):
709
+ """Run a subprocess command that can be cancelled."""
710
+ # Set up environment with constraint file if available
711
+ env = os.environ.copy()
712
+ if constraint_file_path:
713
+ env['PIP_CONSTRAINT'] = constraint_file_path
714
+
715
+ process = subprocess.Popen(
716
+ cmd,
717
+ stdout=subprocess.PIPE,
718
+ stderr=subprocess.PIPE,
719
+ text=True,
720
+ env=env
721
+ )
722
+
723
+ try:
724
+ stdout, stderr = process.communicate(timeout=timeout)
725
+ return process.returncode, stdout, stderr
726
+ except subprocess.TimeoutExpired:
694
727
  process.kill()
695
- try:
696
- process.communicate(timeout=1)
697
- except:
698
- pass
699
- raise
728
+ process.communicate() # Clean up
729
+ raise
730
+ except:
731
+ # If we're interrupted or cancelled, kill the process
732
+ if process.poll() is None: # Process still running
733
+ process.kill()
734
+ try:
735
+ process.communicate(timeout=1)
736
+ except:
737
+ pass
738
+ raise
700
739
 
701
- # Get current editable packages to find source directories
702
- editable_packages = get_editable_packages()
740
+ # Get current editable packages to find source directories
741
+ editable_packages = get_editable_packages()
703
742
 
704
- for package_info in packages_to_update:
705
- # Check for cancellation before processing each package
706
- if cancel_event and cancel_event.is_set():
707
- console.print("[yellow]Update cancelled by user[/yellow]")
708
- break
743
+ for package_info in packages_to_update:
744
+ # Check for cancellation before processing each package
745
+ if cancel_event and cancel_event.is_set():
746
+ console.print("[yellow]Update cancelled by user[/yellow]")
747
+ break
709
748
 
710
- package_name = package_info["name"]
711
- latest_version = package_info.get("latest_version")
712
- is_editable = package_info.get("editable", False)
749
+ package_name = package_info["name"]
750
+ latest_version = package_info.get("latest_version")
751
+ is_editable = package_info.get("editable", False)
713
752
 
714
- try:
715
- console.print(f"Updating {package_name}...")
753
+ try:
754
+ console.print(f"Updating {package_name}...")
716
755
 
717
- if is_editable:
718
- # Package is editable, reinstall from source directory
719
- from packaging.utils import canonicalize_name
720
- canonical_name = canonicalize_name(package_name)
721
- source_path = editable_packages.get(canonical_name)
756
+ if is_editable:
757
+ # Package is editable, reinstall from source directory
758
+ canonical_name = canonicalize_name(package_name)
759
+ source_path = editable_packages.get(canonical_name)
722
760
 
723
- if source_path:
724
- console.print(f" 📝 Reinstalling editable package from: {source_path}")
761
+ if source_path:
762
+ console.print(f" 📝 Reinstalling editable package from: {source_path}")
725
763
 
726
- # First uninstall the current version
727
- uninstall_cmd = [sys.executable, "-m", "pip", "uninstall", package_name, "-y"]
728
- returncode, stdout, stderr = run_with_cancel(uninstall_cmd, timeout=timeout)
764
+ # First uninstall the current version
765
+ uninstall_cmd = [sys.executable, "-m", "pip", "uninstall", package_name, "-y"]
766
+ returncode, stdout, stderr = run_with_cancel(uninstall_cmd, timeout=timeout)
729
767
 
730
- if returncode != 0:
731
- console.print(f" [red]Failed to uninstall {package_name}: {stderr}[/red]")
732
- failed_updates.append(package_name)
733
- continue
768
+ if returncode != 0:
769
+ console.print(f" [red]Failed to uninstall {package_name}: {stderr}[/red]")
770
+ failed_updates.append(package_name)
771
+ continue
772
+
773
+ # Then reinstall in editable mode
774
+ install_cmd = [sys.executable, "-m", "pip", "install", "-e", source_path]
775
+ returncode, stdout, stderr = run_with_cancel(install_cmd, timeout=timeout)
776
+
777
+ if returncode == 0:
778
+ console.print(f" [green]✓ Successfully updated editable {package_name}[/green]")
779
+ successful_updates.append(package_name)
780
+ else:
781
+ console.print(f" [red]Failed to reinstall editable {package_name}: {stderr}[/red]")
782
+ failed_updates.append(package_name)
783
+ else:
784
+ console.print(f" [yellow]Could not find source path for editable {package_name}, updating normally[/yellow]")
785
+ # Fall through to normal update
786
+ is_editable = False
787
+
788
+ if not is_editable:
789
+ # Regular package update
790
+ # Use --upgrade instead of pinning to specific versions to allow pip's
791
+ # dependency resolver to handle interdependent packages correctly
792
+ # (e.g., pydantic and pydantic-core, boto3 and botocore)
793
+ install_cmd = [sys.executable, "-m", "pip", "install", "--upgrade", package_name]
734
794
 
735
- # Then reinstall in editable mode
736
- install_cmd = [sys.executable, "-m", "pip", "install", "-e", source_path]
737
795
  returncode, stdout, stderr = run_with_cancel(install_cmd, timeout=timeout)
738
796
 
739
797
  if returncode == 0:
740
- console.print(f" [green]✓ Successfully updated editable {package_name}[/green]")
798
+ console.print(f" [green]✓ Successfully updated {package_name}[/green]")
741
799
  successful_updates.append(package_name)
742
800
  else:
743
- console.print(f" [red]Failed to reinstall editable {package_name}: {stderr}[/red]")
801
+ console.print(f" [red]Failed to update {package_name}: {stderr}[/red]")
744
802
  failed_updates.append(package_name)
745
- else:
746
- console.print(f" [yellow]Could not find source path for editable {package_name}, updating normally[/yellow]")
747
- # Fall through to normal update
748
- is_editable = False
749
-
750
- if not is_editable:
751
- # Regular package update
752
- if latest_version:
753
- install_cmd = [sys.executable, "-m", "pip", "install", f"{package_name}=={latest_version}"]
754
- else:
755
- install_cmd = [sys.executable, "-m", "pip", "install", "--upgrade", package_name]
756
-
757
- returncode, stdout, stderr = run_with_cancel(install_cmd, timeout=timeout)
758
803
 
759
- if returncode == 0:
760
- console.print(f" [green] Successfully updated {package_name}[/green]")
761
- successful_updates.append(package_name)
762
- else:
763
- console.print(f" [red]Failed to update {package_name}: {stderr}[/red]")
764
- failed_updates.append(package_name)
804
+ except subprocess.TimeoutExpired:
805
+ console.print(f" [red]Timeout updating {package_name}[/red]")
806
+ failed_updates.append(package_name)
807
+ except Exception as e:
808
+ console.print(f" [red]Error updating {package_name}: {e}[/red]")
809
+ failed_updates.append(package_name)
765
810
 
766
- except subprocess.TimeoutExpired:
767
- console.print(f" [red]Timeout updating {package_name}[/red]")
768
- failed_updates.append(package_name)
769
- except Exception as e:
770
- console.print(f" [red]Error updating {package_name}: {e}[/red]")
771
- failed_updates.append(package_name)
811
+ return successful_updates, failed_updates
772
812
 
773
- return successful_updates, failed_updates
813
+ finally:
814
+ # Clean up temporary constraint file
815
+ if constraint_file_path and os.path.exists(constraint_file_path):
816
+ try:
817
+ os.unlink(constraint_file_path)
818
+ except Exception:
819
+ pass # Best effort cleanup
@@ -989,81 +989,116 @@ class PackageUpdateScreen(ModalScreen[None]):
989
989
  try:
990
990
  import subprocess
991
991
  import sys
992
+ import tempfile
993
+ import os
994
+ from packaging.utils import canonicalize_name
992
995
 
993
996
  logger.info(f"Starting batch update for {len(self.selected_packages)} packages")
994
997
  total_packages = len(self.selected_packages)
995
998
 
996
- # Build list of package specs to update, respecting constraints
997
- package_specs = []
998
- package_names = []
999
- for pkg in self.selected_packages:
1000
- package_names.append(pkg["name"])
1001
- # Check if package has a constraint that should be applied instead of latest version
1002
- constraint = pkg.get('constraint')
1003
- if constraint:
1004
- # Apply the constraint instead of pinning to latest version
1005
- spec = f"{pkg['name']}{constraint}"
1006
- else:
1007
- # No constraint, use latest version
1008
- spec = f"{pkg['name']}=={pkg['latest_version']}"
1009
- package_specs.append(spec)
1010
-
1011
- self.app.call_from_thread(self._update_status, f"Updating {total_packages} packages...")
1012
- self.app.call_from_thread(self._log_message, f"{'='*70}")
1013
- self.app.call_from_thread(self._log_message, f"📦 Updating {total_packages} packages: {', '.join(package_names[:5])}")
1014
- if len(package_names) > 5:
1015
- self.app.call_from_thread(self._log_message, f" ... and {len(package_names) - 5} more")
1016
- self.app.call_from_thread(self._log_message, f"{'='*70}\n")
1017
-
1018
- # Prepare pip command to install all packages at once with proper version specs
1019
- pip_cmd = [sys.executable, "-m", "pip", "install"] + package_specs
1020
-
1021
- # Run pip and capture output with proper cleanup
1022
- from ..utils import ManagedProcess
1023
- return_code = None
1024
-
999
+ # Get canonical names of packages being updated
1000
+ from ..package_constraints import read_constraints
1001
+ all_constraints = read_constraints()
1002
+ packages_being_updated = {canonicalize_name(pkg["name"]) for pkg in self.selected_packages}
1003
+
1004
+ # Filter out constraints for packages being updated to avoid conflicts
1005
+ filtered_constraints = {
1006
+ pkg: constraint
1007
+ for pkg, constraint in all_constraints.items()
1008
+ if pkg not in packages_being_updated
1009
+ }
1010
+
1011
+ # Create a temporary constraints file if there are any constraints to apply
1012
+ constraint_file = None
1013
+ constraint_file_path = None
1025
1014
  try:
1026
- with ManagedProcess(
1027
- pip_cmd,
1028
- stdout=subprocess.PIPE,
1029
- stderr=subprocess.STDOUT,
1030
- text=True,
1031
- bufsize=1,
1032
- universal_newlines=True
1033
- ) as process:
1034
- # Stream output line by line
1035
- if process.stdout is None:
1036
- raise RuntimeError("Failed to capture subprocess output")
1037
- for line in process.stdout:
1038
- if self.cancel_event.is_set():
1039
- self.app.call_from_thread(self._log_message, "\n🛑 Update cancelled by user")
1040
- break
1041
- # Display each line of pip output
1042
- self.app.call_from_thread(self._log_message, line.rstrip())
1043
-
1044
- # Wait for process to complete
1045
- return_code = process.wait()
1046
- except Exception as e:
1047
- logger.error(f"Error during package update: {e}")
1048
- self.app.call_from_thread(self._log_message, f"\n❌ Error: {e}")
1049
- return_code = 1
1050
-
1051
- if return_code == 0:
1052
- # All packages updated successfully
1053
- self.successful_updates.extend(package_names)
1054
- self.app.call_from_thread(self._log_message, f"\n{'='*70}")
1055
- self.app.call_from_thread(self._log_message, f"✅ Successfully updated all {total_packages} packages!")
1056
- self.app.call_from_thread(self._log_message, f"{'='*70}")
1057
- else:
1058
- # Some packages failed - pip will have shown which ones in output
1059
- self.failed_updates.extend(package_names)
1060
- self.app.call_from_thread(self._log_message, f"\n{'='*70}")
1061
- self.app.call_from_thread(self._log_message, "❌ Update completed with errors (see above)")
1015
+ if filtered_constraints:
1016
+ constraint_file = tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False)
1017
+ constraint_file_path = constraint_file.name
1018
+ for pkg, constraint in filtered_constraints.items():
1019
+ constraint_file.write(f"{pkg}{constraint}\n")
1020
+ constraint_file.close()
1021
+ self.app.call_from_thread(self._log_message, f"[dim]Using filtered constraints (excluding {len(packages_being_updated)} package(s) being updated)[/dim]")
1022
+
1023
+ # Build list of package names to update
1024
+ # Use --upgrade instead of pinning versions to avoid dependency conflicts
1025
+ # when updating interdependent packages (e.g., pydantic and pydantic-core)
1026
+ package_names = []
1027
+ for pkg in self.selected_packages:
1028
+ package_names.append(pkg["name"])
1029
+
1030
+ self.app.call_from_thread(self._update_status, f"Updating {total_packages} packages...")
1062
1031
  self.app.call_from_thread(self._log_message, f"{'='*70}")
1063
-
1064
- # Show final results and cleanup
1065
- logger.info("Update loop completed, calling _update_complete")
1066
- self.app.call_from_thread(self._update_complete)
1032
+ self.app.call_from_thread(self._log_message, f"📦 Updating {total_packages} packages: {', '.join(package_names[:5])}")
1033
+ if len(package_names) > 5:
1034
+ self.app.call_from_thread(self._log_message, f" ... and {len(package_names) - 5} more")
1035
+ self.app.call_from_thread(self._log_message, f"{'='*70}\n")
1036
+
1037
+ # Prepare pip command to install all packages with --upgrade
1038
+ # This allows pip's dependency resolver to find compatible versions
1039
+ # for interdependent packages (e.g., pydantic requires specific pydantic-core)
1040
+ pip_cmd = [sys.executable, "-m", "pip", "install", "--upgrade"] + package_names
1041
+
1042
+ # Set up environment with constraint file if available
1043
+ env = os.environ.copy()
1044
+ if constraint_file_path:
1045
+ env['PIP_CONSTRAINT'] = constraint_file_path
1046
+
1047
+ # Run pip and capture output with proper cleanup
1048
+ from ..utils import ManagedProcess
1049
+ return_code = None
1050
+
1051
+ try:
1052
+ with ManagedProcess(
1053
+ pip_cmd,
1054
+ stdout=subprocess.PIPE,
1055
+ stderr=subprocess.STDOUT,
1056
+ text=True,
1057
+ bufsize=1,
1058
+ universal_newlines=True,
1059
+ env=env
1060
+ ) as process:
1061
+ # Stream output line by line
1062
+ if process.stdout is None:
1063
+ raise RuntimeError("Failed to capture subprocess output")
1064
+ for line in process.stdout:
1065
+ if self.cancel_event.is_set():
1066
+ self.app.call_from_thread(self._log_message, "\n🛑 Update cancelled by user")
1067
+ break
1068
+ # Display each line of pip output
1069
+ self.app.call_from_thread(self._log_message, line.rstrip())
1070
+
1071
+ # Wait for process to complete
1072
+ return_code = process.wait()
1073
+ except Exception as e:
1074
+ logger.error(f"Error during package update: {e}")
1075
+ self.app.call_from_thread(self._log_message, f"\n❌ Error: {e}")
1076
+ return_code = 1
1077
+
1078
+ if return_code == 0:
1079
+ # All packages updated successfully
1080
+ self.successful_updates.extend(package_names)
1081
+ self.app.call_from_thread(self._log_message, f"\n{'='*70}")
1082
+ self.app.call_from_thread(self._log_message, f"✅ Successfully updated all {total_packages} packages!")
1083
+ self.app.call_from_thread(self._log_message, f"{'='*70}")
1084
+ else:
1085
+ # Some packages failed - pip will have shown which ones in output
1086
+ self.failed_updates.extend(package_names)
1087
+ self.app.call_from_thread(self._log_message, f"\n{'='*70}")
1088
+ self.app.call_from_thread(self._log_message, "❌ Update completed with errors (see above)")
1089
+ self.app.call_from_thread(self._log_message, f"{'='*70}")
1090
+
1091
+ # Show final results and cleanup
1092
+ logger.info("Update loop completed, calling _update_complete")
1093
+ self.app.call_from_thread(self._update_complete)
1094
+
1095
+ finally:
1096
+ # Clean up temporary constraint file
1097
+ if constraint_file_path and os.path.exists(constraint_file_path):
1098
+ try:
1099
+ os.unlink(constraint_file_path)
1100
+ except Exception:
1101
+ pass # Best effort cleanup
1067
1102
 
1068
1103
  except Exception as e:
1069
1104
  logger.error(f"Error in update loop: {e}", exc_info=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipu-cli
3
- Version: 0.1.dev3
3
+ Version: 0.1.dev5
4
4
  Summary: A cute Python package updater
5
5
  Author-email: Scott Arne Johnson <scott.arne.johnson@gmail.com>
6
6
  License-Expression: MIT
@@ -1,19 +1,19 @@
1
- pipu_cli/__init__.py,sha256=ZGWSFbUBHC0Pkg98JvTSu7BYa8nE73GgCi8vvlDrNxQ,1190
1
+ pipu_cli/__init__.py,sha256=c6A1uB6wkExOK4rptn9LBAd6FaSBplB1jxURCwoNFWY,1190
2
2
  pipu_cli/cli.py,sha256=aCN2Fz1bWhXs8MC7kqy5GdzJggNtTdumW4PQkxKOBpg,39993
3
3
  pipu_cli/common.py,sha256=g5krfXFsvVJpOf87Vw4DGi5WIduDgMlRuONKXqO328M,78
4
4
  pipu_cli/config.py,sha256=xsfNU4ORAujla_FGfsMKpxy7QTpd_bJhRF_u4IPKLW0,3635
5
- pipu_cli/internals.py,sha256=1Fds2mMn2SJwfTwfjbeqAcTWJjOazP9QtrjRvgzzspo,32786
5
+ pipu_cli/internals.py,sha256=m_aF-yrOT5UJt54vv6X2gEukgokFXkz0m9SAKRofir0,35101
6
6
  pipu_cli/package_constraints.py,sha256=6LSFlC93HNIFym7dYkfYn0fsOic6QDe1ADyghK94Pk8,93052
7
7
  pipu_cli/thread_safe.py,sha256=zdQyCoMVJW73MC-d1pL_4ZO-K4AwkI0JeVyQsd8x7nY,7545
8
8
  pipu_cli/utils.py,sha256=ijSHKVuKbjmRbj2RwD9S1606PeY4oDiutzhutpX25wM,5842
9
9
  pipu_cli/ui/__init__.py,sha256=nCb_3G_vZXy5_Or9z9r-3XhYV1ppUR1r7nMZ9_6Srwg,1432
10
10
  pipu_cli/ui/apps.py,sha256=ltH24sg-3nqVpoomgCwhVYuAwq3hBUwYRH60JXtV2Yg,59771
11
11
  pipu_cli/ui/constants.py,sha256=HBPf4KYWHiT18c7ciQ0HeI7gZE3VIFOT0uobLU8YxQA,445
12
- pipu_cli/ui/modal_dialogs.py,sha256=5OF8UL2lXXH3GoAxFQCqp_T3jc5jeLSk77Y7ffBt6mw,45849
12
+ pipu_cli/ui/modal_dialogs.py,sha256=-Trh60n2mev6Pr-aIvetJHnXqqoJaVDmA3c8xH_xePM,47974
13
13
  pipu_cli/ui/table_widgets.py,sha256=PC0CqqrK3KgMbTCYRPG01lxkEtRBz9xr9PN8EzoObz8,14322
14
- pipu_cli-0.1.dev3.dist-info/licenses/LICENSE,sha256=q6TxVbSI0WMB9ulF2V0FWQfeA5om3d-T9X7QwuhdiYE,1075
15
- pipu_cli-0.1.dev3.dist-info/METADATA,sha256=9NjeUzT8buLXYRHBUAuQPulDnA0sLi66eov4XldR57c,13200
16
- pipu_cli-0.1.dev3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
- pipu_cli-0.1.dev3.dist-info/entry_points.txt,sha256=VSv6od00zOPblnFPflNLaci4jBtQIgLYJjL1BKxLz_o,42
18
- pipu_cli-0.1.dev3.dist-info/top_level.txt,sha256=z3Yce93-jGQjGRpsGZUZvbS8osh3OyS7MVpzG0uBE5M,9
19
- pipu_cli-0.1.dev3.dist-info/RECORD,,
14
+ pipu_cli-0.1.dev5.dist-info/licenses/LICENSE,sha256=q6TxVbSI0WMB9ulF2V0FWQfeA5om3d-T9X7QwuhdiYE,1075
15
+ pipu_cli-0.1.dev5.dist-info/METADATA,sha256=uV-3D56gMhQSSe8aK0V1t7gUPSSihd0pHWsjObLC6pA,13200
16
+ pipu_cli-0.1.dev5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ pipu_cli-0.1.dev5.dist-info/entry_points.txt,sha256=VSv6od00zOPblnFPflNLaci4jBtQIgLYJjL1BKxLz_o,42
18
+ pipu_cli-0.1.dev5.dist-info/top_level.txt,sha256=z3Yce93-jGQjGRpsGZUZvbS8osh3OyS7MVpzG0uBE5M,9
19
+ pipu_cli-0.1.dev5.dist-info/RECORD,,