@@ -1464,11 +1464,13 @@ type LockDependenciesResult struct {
14641464 RequirementsPath string
14651465 PythonVersion string
14661466 InputPath string
1467+ DependencyType python.DependencyType
1468+ ProjectRoot string
14671469}
14681470
1469- // FindRequirementsPath finds the requirements.txt path for the given input.
1470- // It handles both direct requirements.txt files and Python assets.
1471- func (l * LockDependenciesCommand ) FindRequirementsPath (inputPath string ) (* LockDependenciesResult , error ) {
1471+ // FindDependencyPath finds the dependency configuration for the given input.
1472+ // It handles direct requirements.txt files, pyproject.toml projects, and Python assets.
1473+ func (l * LockDependenciesCommand ) FindDependencyPath (inputPath string ) (* LockDependenciesResult , error ) {
14721474 // Get absolute path
14731475 absolutePath , err := filepath .Abs (inputPath )
14741476 if err != nil {
@@ -1481,53 +1483,67 @@ func (l *LockDependenciesCommand) FindRequirementsPath(inputPath string) (*LockD
14811483 return nil , errors2 .Wrap (err , "failed to find repository" )
14821484 }
14831485
1484- var requirementsTxt string
1485-
14861486 // Check if input is a requirements.txt file directly
14871487 if strings .HasSuffix (inputPath , "requirements.txt" ) {
1488- requirementsTxt = absolutePath
1489- } else {
1490- // Parse the asset
1491- asset , err := l . builder . CreateAssetFromFile ( absolutePath , nil )
1492- if err != nil {
1493- return nil , errors2 . Wrap ( err , "failed to parse asset" )
1494- }
1488+ return & LockDependenciesResult {
1489+ RequirementsPath : absolutePath ,
1490+ InputPath : absolutePath ,
1491+ DependencyType : python . DependencyTypeRequirementsTxt ,
1492+ ProjectRoot : filepath . Dir ( absolutePath ),
1493+ }, nil
1494+ }
14951495
1496- if asset == nil {
1497- return nil , errors2 .New ("file is not a valid asset" )
1498- }
1496+ // Check if input is a pyproject.toml file directly
1497+ if strings .HasSuffix (inputPath , "pyproject.toml" ) {
1498+ return & LockDependenciesResult {
1499+ InputPath : absolutePath ,
1500+ DependencyType : python .DependencyTypePyproject ,
1501+ ProjectRoot : filepath .Dir (absolutePath ),
1502+ }, nil
1503+ }
14991504
1500- // Check if asset is a Python asset
1501- if asset .Type != pipeline .AssetTypePython && ! strings .HasSuffix (asset .ExecutableFile .Path , ".py" ) {
1502- return nil , errors2 .New ("asset is not a Python asset" )
1503- }
1505+ // Parse the asset
1506+ asset , err := l .builder .CreateAssetFromFile (absolutePath , nil )
1507+ if err != nil {
1508+ return nil , errors2 .Wrap (err , "failed to parse asset" )
1509+ }
15041510
1505- // Find requirements.txt
1506- moduleFinder := & python.ModulePathFinder {}
1507- requirementsTxt , err = moduleFinder .FindRequirementsTxtInPath (repo .Path , & asset .ExecutableFile )
1508- if err != nil {
1509- var noReqsError * python.NoRequirementsFoundError
1510- if errors .As (err , & noReqsError ) {
1511- return nil , errors2 .New ("no requirements.txt found for this asset" )
1512- }
1513- return nil , errors2 .Wrap (err , "failed to find requirements.txt" )
1514- }
1511+ if asset == nil {
1512+ return nil , errors2 .New ("file is not a valid asset" )
1513+ }
1514+
1515+ // Check if asset is a Python asset
1516+ if asset .Type != pipeline .AssetTypePython && ! strings .HasSuffix (asset .ExecutableFile .Path , ".py" ) {
1517+ return nil , errors2 .New ("asset is not a Python asset" )
1518+ }
1519+
1520+ // Find dependency configuration
1521+ moduleFinder := & python.ModulePathFinder {}
1522+ depConfig , err := moduleFinder .FindDependencyConfig (repo .Path , & asset .ExecutableFile )
1523+ if err != nil {
1524+ return nil , errors2 .Wrap (err , "failed to find dependency configuration" )
1525+ }
1526+
1527+ if depConfig .Type == python .DependencyTypeNone {
1528+ return nil , errors2 .New ("no dependency configuration found for this asset (no requirements.txt or pyproject.toml)" )
15151529 }
15161530
15171531 return & LockDependenciesResult {
1518- RequirementsPath : requirementsTxt ,
1532+ RequirementsPath : depConfig . RequirementsTxt ,
15191533 InputPath : absolutePath ,
1534+ DependencyType : depConfig .Type ,
1535+ ProjectRoot : depConfig .ProjectRoot ,
15201536 }, nil
15211537}
15221538
15231539// LockAssetDependencies returns a CLI command that locks Python dependencies for an asset.
1524- // It finds the asset's requirements file, creates/uses a uv environment, and updates the
1525- // requirements file with locked versions using uv pip compile .
1540+ // It finds the asset's dependency configuration and locks versions using either
1541+ // uv pip compile (for requirements.txt) or uv lock (for pyproject.toml) .
15261542func LockAssetDependencies () * cli.Command {
15271543 return & cli.Command {
15281544 Name : "lock-asset-dependencies" ,
15291545 Usage : "Lock Python dependencies for a Bruin asset using uv" ,
1530- ArgsUsage : "[path to the asset definition or requirements.txt]" ,
1546+ ArgsUsage : "[path to the asset definition, requirements.txt, or pyproject.toml ]" ,
15311547 Flags : []cli.Flag {
15321548 & cli.StringFlag {
15331549 Name : "python-version" ,
@@ -1560,26 +1576,19 @@ func LockAssetDependencies() *cli.Command {
15601576 return cli .Exit ("" , 1 )
15611577 }
15621578
1563- // Use the refactored command to find requirements path
1579+ // Find dependency configuration
15641580 cmd := & LockDependenciesCommand {
15651581 builder : DefaultPipelineBuilder ,
15661582 }
15671583
1568- result , err := cmd .FindRequirementsPath (assetPath )
1584+ result , err := cmd .FindDependencyPath (assetPath )
15691585 if err != nil {
15701586 printErrorJSON (err )
15711587 return cli .Exit ("" , 1 )
15721588 }
15731589
15741590 result .PythonVersion = pythonVersion
15751591
1576- // Find the git repo for working directory
1577- repo , err := git .FindRepoFromPath (result .InputPath )
1578- if err != nil {
1579- printErrorJSON (errors2 .Wrap (err , "failed to find repository" ))
1580- return cli .Exit ("" , 1 )
1581- }
1582-
15831592 // Ensure uv is installed
15841593 uvChecker := & uv.Checker {}
15851594 uvBinaryPath , err := uvChecker .EnsureUvInstalled (ctx )
@@ -1588,36 +1597,66 @@ func LockAssetDependencies() *cli.Command {
15881597 return cli .Exit ("" , 1 )
15891598 }
15901599
1591- // Run uv pip compile to lock dependencies
1592- // uv pip compile requirements.txt -o requirements.txt --python-version X.Y --no-header
1593- execCmd := exec .CommandContext (ctx , uvBinaryPath , "pip" , "compile" , result .RequirementsPath , "-o" , result .RequirementsPath , "--python-version" , pythonVersion , "--quiet" , "--no-header" ) //nolint:gosec
1594- execCmd .Dir = repo .Path
1600+ var execCmd * exec.Cmd
1601+ switch result .DependencyType { //nolint:exhaustive
1602+ case python .DependencyTypePyproject :
1603+ // uv lock --python <version>
1604+ execCmd = exec .CommandContext (ctx , uvBinaryPath , "lock" , "--python" , pythonVersion ) //nolint:gosec
1605+ execCmd .Dir = result .ProjectRoot
1606+
1607+ case python .DependencyTypeRequirementsTxt :
1608+ // uv pip compile requirements.txt -o requirements.txt --python-version X.Y --no-header
1609+ execCmd = exec .CommandContext (ctx , uvBinaryPath , "pip" , "compile" , result .RequirementsPath , "-o" , result .RequirementsPath , "--python-version" , pythonVersion , "--quiet" , "--no-header" ) //nolint:gosec
1610+ execCmd .Dir = result .ProjectRoot
1611+
1612+ default :
1613+ printErrorJSON (errors2 .New ("no supported dependency configuration found" ))
1614+ return cli .Exit ("" , 1 )
1615+ }
1616+
15951617 execCmd .Stdout = os .Stdout
15961618 execCmd .Stderr = os .Stderr
15971619
15981620 err = execCmd .Run ()
15991621 if err != nil {
1600- printErrorJSON (errors2 .Wrap (err , "failed to lock dependencies with uv pip compile" ))
1622+ if result .DependencyType == python .DependencyTypePyproject {
1623+ printErrorJSON (errors2 .Wrap (err , "failed to lock dependencies with uv lock" ))
1624+ } else {
1625+ printErrorJSON (errors2 .Wrap (err , "failed to lock dependencies with uv pip compile" ))
1626+ }
16011627 return cli .Exit ("" , 1 )
16021628 }
16031629
16041630 // Output result
16051631 switch output {
16061632 case outputFormatPlain :
1607- fmt .Printf ("Successfully locked dependencies in %s\n " , result .RequirementsPath )
1633+ if result .DependencyType == python .DependencyTypePyproject {
1634+ fmt .Printf ("Successfully locked dependencies in %s\n " , filepath .Join (result .ProjectRoot , "uv.lock" ))
1635+ } else {
1636+ fmt .Printf ("Successfully locked dependencies in %s\n " , result .RequirementsPath )
1637+ }
16081638 case "json" :
16091639 type jsonResponse struct {
1610- RequirementsPath string `json:"requirements_path"`
1640+ RequirementsPath string `json:"requirements_path,omitempty"`
1641+ LockFilePath string `json:"lock_file_path,omitempty"`
16111642 PythonVersion string `json:"python_version"`
16121643 AssetPath string `json:"asset_path"`
1644+ DependencyType string `json:"dependency_type"`
16131645 Success bool `json:"success"`
16141646 }
16151647
16161648 finalOutput := jsonResponse {
1617- RequirementsPath : result .RequirementsPath ,
1618- PythonVersion : pythonVersion ,
1619- AssetPath : result .InputPath ,
1620- Success : true ,
1649+ PythonVersion : pythonVersion ,
1650+ AssetPath : result .InputPath ,
1651+ Success : true ,
1652+ }
1653+
1654+ if result .DependencyType == python .DependencyTypePyproject {
1655+ finalOutput .DependencyType = "pyproject"
1656+ finalOutput .LockFilePath = filepath .Join (result .ProjectRoot , "uv.lock" )
1657+ } else {
1658+ finalOutput .DependencyType = "requirements_txt"
1659+ finalOutput .RequirementsPath = result .RequirementsPath
16211660 }
16221661
16231662 jsonData , err := json .Marshal (finalOutput )
0 commit comments