-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
231 lines (194 loc) · 6.06 KB
/
main.py
File metadata and controls
231 lines (194 loc) · 6.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
import argparse
import logging
from typing import Callable, Literal, Annotated
from pydantic import BaseModel, Field
from pydantic_ai import Agent
from mcp.server.fastmcp import FastMCP
from copr.v3 import Client
class Project(BaseModel):
id: int
web_url: str
ownername: str
name: str
full_name: str
class BuildStatus(BaseModel):
id: int
state: str
name: str | None = None
class Build(BaseModel):
id: int
web_url: str
state: str
submitter: str
class BuildFromDistGit(BaseModel):
"""
Use this schema when you want to build a package Fedora or any other
DistGit instance
"""
source_type: Literal["distgit"] = "distgit"
packagename: str
namespace: str | None = None
class BuildFromPyPI(BaseModel):
"""
Use this schema when you want to build a package from PyPI
"""
source_type: Literal["pypi"] = "pypi"
packagename: str
spec_template: str | None = None
# We need to annotate this with a discriminator otherwise AI sometimes doesn't
# know what type to use and therefore uses the first one.
# class to use
BuildSource = Annotated[
BuildFromDistGit | BuildFromPyPI,
Field(discriminator="source_type"),
]
logging.basicConfig(level=logging.WARNING)
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
def copr_create_project(
ownername: str,
projectname: str,
chroots: list[str],
) -> Project:
"""
Create a Copr project with a given name for a specified owner.
When creating a new project, at least one chroot must be specified. For
example `fedora-rawhide-x86_64`
"""
log.debug("copr_create_project: %s/%s", ownername, projectname)
client = Client.create_from_config_file()
project = client.project_proxy.add(ownername, projectname, chroots)
# Taken from copr-cli action_create
# This should be either part of python-copr or returned by the API
owner_part = project.ownername.replace("@", "g/")
web_url = "/".join([
client.config["copr_url"].strip("/"),
"coprs", owner_part, project.name, "",
])
return Project(
id=project.id,
web_url=web_url,
ownername=project.ownername,
name=project.name,
full_name=project.full_name,
)
def copr_build_status(build_id: int) -> BuildStatus:
"""
Get the status of a Copr build by its ID.
"""
log.debug("copr_build_status: %s", build_id)
client = Client.create_from_config_file()
build = client.build_proxy.get(build_id)
return BuildStatus(
id=build.id,
state=build.state,
)
def copr_list_builds(ownername: str, projectname: str) -> list[BuildStatus]:
"""
Get the status of all builds in a Copr project identified by its
ownername/projectname.
"""
log.debug("copr_list_builds: %s/%s", ownername, projectname)
client = Client.create_from_config_file()
builds = client.build_proxy.get_list(ownername, projectname)
return [
BuildStatus(
id=build.id,
state=build.state,
name=build.source_package["name"],
)
for build in builds
]
def copr_submit_build(
ownername: str,
projectname: str,
source: BuildSource,
) -> Build:
"""
Submit a new build into a Copr project defined by its ownername and
projectname. Copr supports multiple source types, see the documentation
https://docs.copr.fedorainfracloud.org/user_documentation.html#build-source-types
"""
log.debug(
"copr_submit_build: %s/%s %s",
ownername, projectname, source.__class__.__name__,
)
client = Client.create_from_config_file()
match source:
case BuildFromDistGit():
build = client.build_proxy.create_from_distgit(
ownername,
projectname,
source.packagename,
namespace=source.namespace,
)
case BuildFromPyPI():
build = client.build_proxy.create_from_pypi(
ownername,
projectname,
source.packagename,
spec_template=source.spec_template,
)
web_url = "/".join([
client.config["copr_url"].strip("/"),
"coprs/build",
str(build.id),
])
return Build(
id=build.id,
web_url=web_url,
state=build.state,
submitter=build.submitter,
)
def copr_enable_repository(ownername: str, projectname: str) -> str:
"""
Provide instructions for enabling a Copr repository on the user system.
This requires root privileges and must be run manually by the user.
"""
return (
f"This action requires root privileges and therefore requires a manual"
f"step from the user.\n"
f"To enable this Copr repository run the following command:\n\n"
f" sudo dnf copr enable {ownername}/{projectname}"
)
def run_mcp(tools: list[Callable], args):
mcp = FastMCP("copr")
for tool in tools:
mcp.add_tool(tool)
mcp.run()
def run_prompt(tools: list[Callable], args):
instructions = (
"You help manage Copr builds. Use tools to get real information.",
)
agent = Agent(
"anthropic:claude-opus-4-6",
instructions=instructions,
)
for tool in tools:
agent.tool_plain(tool)
result = agent.run_sync(args.prompt)
print(result.output)
def main():
parser = argparse.ArgumentParser(description="Copr AI assistant")
parser.add_argument(
"--prompt",
help="Don't run MCP and send a prompt directly",
)
args = parser.parse_args()
client = Client.create_from_config_file()
tools = [
copr_build_status,
copr_list_builds,
copr_create_project,
copr_submit_build,
copr_enable_repository,
# We probably don't have to implement wrappers around every python-copr
# function. We can simply register the client methods like this.
client.base_proxy.auth_check,
]
if args.prompt:
run_prompt(tools, args)
else:
run_mcp(tools, args)
if __name__ == "__main__":
main()