p4cmd 2.8.6__tar.gz → 2.9.0__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.
Files changed (36) hide show
  1. p4cmd-2.9.0/PKG-INFO +493 -0
  2. p4cmd-2.9.0/README.md +469 -0
  3. {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/__init__.py +1 -1
  4. {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/p4cmd.py +215 -221
  5. p4cmd-2.9.0/p4cmd/p4errors.py +25 -0
  6. p4cmd-2.9.0/p4cmd/p4file.py +298 -0
  7. {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/utils.py +21 -22
  8. p4cmd-2.9.0/p4cmd.egg-info/PKG-INFO +493 -0
  9. {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd.egg-info/SOURCES.txt +8 -1
  10. p4cmd-2.9.0/p4cmd.egg-info/requires.txt +3 -0
  11. {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd.egg-info/top_level.txt +1 -0
  12. {p4cmd-2.8.6 → p4cmd-2.9.0}/setup.py +1 -0
  13. p4cmd-2.9.0/tests/__init__.py +0 -0
  14. p4cmd-2.9.0/tests/conftest.py +32 -0
  15. p4cmd-2.9.0/tests/test_integration.py +480 -0
  16. p4cmd-2.9.0/tests/test_p4client.py +573 -0
  17. p4cmd-2.9.0/tests/test_p4file.py +388 -0
  18. p4cmd-2.9.0/tests/test_utils.py +119 -0
  19. p4cmd-2.8.6/PKG-INFO +0 -211
  20. p4cmd-2.8.6/README.md +0 -190
  21. p4cmd-2.8.6/p4cmd/p4errors.py +0 -15
  22. p4cmd-2.8.6/p4cmd/p4file.py +0 -242
  23. p4cmd-2.8.6/p4cmd.egg-info/PKG-INFO +0 -211
  24. {p4cmd-2.8.6 → p4cmd-2.9.0}/LICENSE +0 -0
  25. {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/__init__.py +0 -0
  26. {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/perforce_DEPOT_ONLY.png +0 -0
  27. {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/perforce_MOVED.png +0 -0
  28. {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/perforce_NEED_SYNC.png +0 -0
  29. {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/perforce_OPEN_FOR_ADD.png +0 -0
  30. {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/perforce_OPEN_FOR_DELETE.png +0 -0
  31. {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/perforce_OPEN_FOR_EDIT.png +0 -0
  32. {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/perforce_UNKNOWN.png +0 -0
  33. {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/perforce_UNTRACKED.png +0 -0
  34. {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd/resources/perforce_UP_TO_DATE.png +0 -0
  35. {p4cmd-2.8.6 → p4cmd-2.9.0}/p4cmd.egg-info/dependency_links.txt +0 -0
  36. {p4cmd-2.8.6 → p4cmd-2.9.0}/setup.cfg +0 -0
p4cmd-2.9.0/PKG-INFO ADDED
@@ -0,0 +1,493 @@
1
+ Metadata-Version: 2.4
2
+ Name: p4cmd
3
+ Version: 2.9.0
4
+ Summary: Simple P4 python module
5
+ Home-page: https://github.com/nielsvaes/p4cmd
6
+ Author: Niels Vaes
7
+ Author-email: nielsvaes@gmail.com
8
+ License: MIT
9
+ Classifier: Operating System :: OS Independent
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Provides-Extra: test
13
+ Requires-Dist: pytest>=7.0; extra == "test"
14
+ Dynamic: author
15
+ Dynamic: author-email
16
+ Dynamic: classifier
17
+ Dynamic: description
18
+ Dynamic: description-content-type
19
+ Dynamic: home-page
20
+ Dynamic: license
21
+ Dynamic: license-file
22
+ Dynamic: provides-extra
23
+ Dynamic: summary
24
+
25
+ ### P4CMD 🌴
26
+
27
+ A Python Perforce package that doesn't bring in any other packages to work.
28
+ Relies on the `p4` CLI installed on the system.
29
+
30
+ ```bash
31
+ pip install p4cmd
32
+ ```
33
+
34
+ ## Getting started
35
+
36
+ ### Creating a client
37
+
38
+ Pass the root path of your Perforce workspace, or use the `P4ROOT` environment variable:
39
+
40
+ ```python
41
+ from p4cmd import p4cmd
42
+
43
+ # Direct path
44
+ p4 = p4cmd.P4Client("~/p4/MyGame")
45
+
46
+ # Or with explicit credentials
47
+ p4 = p4cmd.P4Client("~/p4/MyGame", user="my.user", client="my-workspace", server="ssl:perforce:1666")
48
+ ```
49
+
50
+ ```python
51
+ import os
52
+ from p4cmd import p4cmd
53
+
54
+ os.environ["P4ROOT"] = "~/p4/MyGame"
55
+ p4 = p4cmd.P4Client.from_env()
56
+ ```
57
+
58
+ If the path you pass doesn't contain a `.p4config` file, P4Client will walk up the directory tree until it finds one and use that as `perforce_root`.
59
+
60
+ ## P4Client API
61
+
62
+ ### Connection & settings
63
+
64
+ | Method | Description |
65
+ |---|---|
66
+ | `host_online(timeout=2.0)` | Check if the Perforce server is reachable |
67
+ | `get_ticket_expiration()` | Seconds until the authentication ticket expires |
68
+ | `get_p4_setting(setting)` | Read a Perforce setting (e.g. `"P4CLIENT"`) |
69
+ | `find_p4_client()` | Return the current workspace name |
70
+ | `find_p4_port()` | Return the server address |
71
+ | `set_workspace(workspace)` | Switch the active workspace |
72
+ | `set_perforce_root(root)` | Change the workspace root directory |
73
+ | `set_max_parallel_connections(value)` | Set max parallel connections for sync/submit (default 4) |
74
+ | `get_all_workspaces()` | List all workspaces for the current user |
75
+
76
+ ### File operations
77
+
78
+ | Method | Description |
79
+ |---|---|
80
+ | `add_or_edit_files(file_list, changelist="default")` | Add new files or check out existing ones — auto-detects which |
81
+ | `add_or_edit_folders(folders, include_subfolders=True, changelist="default")` | Same as above, for entire folders |
82
+ | `add_files(file_list, changelist="default")` | Mark files for add |
83
+ | `edit_files(file_list, changelist="default")` | Mark files for edit |
84
+ | `delete_files(file_list, changelist="default")` | Mark files for delete |
85
+ | `rename_file(old_path, new_path, changelist="default")` | P4 move/rename a file |
86
+ | `copy_file(original_path, copied_path, changelist="default")` | P4 copy a file |
87
+ | `revert_files(file_list, unchanged_only=False)` | Revert files |
88
+ | `revert_folders(folder_list, unchanged_only=False)` | Revert all files in folders |
89
+ | `sync_files(file_list, revision=-1, verify=True, force=False)` | Sync files to a specific revision |
90
+ | `sync_folders(folder_list)` | Sync entire folders |
91
+ | `reconcile_offline_files(file_list, add=True, edit=True, delete=True, changelist="default")` | Reconcile offline changes |
92
+ | `reconcile_offline_folders(folder_list, add=True, edit=True, delete=True, changelist="default")` | Reconcile offline changes in folders |
93
+
94
+ ### Changelists
95
+
96
+ | Method | Description |
97
+ |---|---|
98
+ | `make_new_changelist(description)` | Create a new numbered changelist |
99
+ | `get_or_make_changelist(description, case_sensitive=False)` | Find a CL by description, or create it |
100
+ | `changelist_exists(changelist)` | Check if a CL exists (by number or description) |
101
+ | `update_changelist_description(changelist, new_description)` | Update a CL's description |
102
+ | `delete_changelist(changelist, ...)` | Delete a CL (with options to obliterate, delete shelved files) |
103
+ | `get_pending_changelists(description_filter="", ...)` | List pending CLs, optionally filtered by description |
104
+ | `get_files_in_changelist(changelist="default")` | List depot paths in a CL |
105
+ | `get_all_files_in_all_changelists()` | List depot paths across all pending CLs |
106
+ | `get_changelist_for_file(depot_path)` | Find which CL contains a file |
107
+ | `move_files_to_changelist(file_list, changelist="default")` | Move files between changelists |
108
+ | `combine_changelists(source_changelists, target_changelist)` | Merge multiple CLs into one |
109
+ | `revert_changelist(unchanged_only=False, changelist="default")` | Revert all files in a CL |
110
+ | `submit_changelist(changelist, revert_unchanged_files=True)` | Submit a CL |
111
+
112
+ ### Shelving
113
+
114
+ | Method | Description |
115
+ |---|---|
116
+ | `shelve_files(changelist, file_list=None, revert_after_shelve=False, force=False)` | Shelve files |
117
+ | `unshelve_files(changelist, file_list=None, target_changelist="default", force=False, delete_shelved_files=False)` | Unshelve files |
118
+ | `get_shelved_files()` | List all shelved files and their CLs |
119
+ | `delete_shelf(changelist)` | Delete shelved files without deleting the CL |
120
+
121
+ ### Path conversion & history
122
+
123
+ | Method | Description |
124
+ |---|---|
125
+ | `get_depot_paths(paths)` | Convert local paths to depot paths |
126
+ | `get_local_paths(paths)` | Convert depot paths to local paths |
127
+ | `get_history(paths)` | Get change history for files |
128
+
129
+ ### P4File conversion
130
+
131
+ | Method | Description |
132
+ |---|---|
133
+ | `files_to_p4files(file_list, allow_invalid_files=False)` | Convert file paths to `P4File` objects with full status info |
134
+ | `folder_to_p4files(folder, include_subfolders=True, allow_invalid_files=False, specific_file_filter="")` | Same, for all files in a folder |
135
+
136
+ ## P4File
137
+
138
+ `P4File` represents a single file and its Perforce status. You get these from `files_to_p4files` or `folder_to_p4files`. All attributes are available as both properties and legacy `get_`/`set_` methods.
139
+
140
+ ### Properties
141
+
142
+ | Property | Type | Description |
143
+ |---|---|---|
144
+ | `local_file_path` | `str` | Local filesystem path |
145
+ | `depot_file_path` | `str` | Depot path (`//depot/...`) |
146
+ | `action` | `str` | Current open action (`add`, `edit`, `delete`, `move/add`, `move/delete`) |
147
+ | `head_action` | `str` | Head revision action |
148
+ | `have_revision` | `int` | Local revision number |
149
+ | `head_revision` | `int` | Latest depot revision number |
150
+ | `last_submit_time` | `str` | Formatted timestamp of last submission |
151
+ | `last_submitted_by` | `str` | User who last submitted |
152
+ | `checked_out_by` | `list` | Users who have the file checked out |
153
+ | `file_size` | `str` | Raw file size from depot |
154
+ | `raw_data` | `str` | Raw fstat data |
155
+
156
+ ### Status predicates
157
+
158
+ | Method | Description |
159
+ |---|---|
160
+ | `get_status()` | Returns a `Status` constant (see below) |
161
+ | `is_open_for_add()` | File is marked for add |
162
+ | `is_open_for_edit()` | File is marked for edit |
163
+ | `is_marked_for_delete()` | File is marked for delete |
164
+ | `is_checked_out()` | File has any open action |
165
+ | `is_untracked()` / `is_local_only()` | File exists locally but not in depot |
166
+ | `is_depot_only()` | File exists in depot but was never synced |
167
+ | `is_deleted()` | Head revision is a delete |
168
+ | `is_moved_deleted()` | File was moved away |
169
+ | `is_moved_added()` | File was moved here |
170
+ | `is_up_to_date()` | Local and head revisions match |
171
+ | `is_under_client_root()` | File is within the workspace mapping |
172
+ | `is_valid()` | Has at least one path set |
173
+ | `needs_syncing()` | Behind head revision and needs sync |
174
+ | `get_file_size(in_megabyte=True)` | File size in MB or bytes |
175
+
176
+ ### Status constants
177
+
178
+ ```python
179
+ from p4cmd.p4file import Status
180
+
181
+ Status.OPEN_FOR_ADD
182
+ Status.OPEN_FOR_EDIT
183
+ Status.OPEN_FOR_DELETE
184
+ Status.NEED_SYNC
185
+ Status.DEPOT_ONLY
186
+ Status.UP_TO_DATE
187
+ Status.UNTRACKED
188
+ Status.MOVED
189
+ Status.DELETED
190
+ Status.MOVED_DELETED
191
+ Status.UNKNOWN
192
+ ```
193
+
194
+ ### Other methods
195
+
196
+ | Method | Description |
197
+ |---|---|
198
+ | `update_self(p4client)` | Refresh all attributes from the server |
199
+ | `update_last_submitted_by(p4client)` | Refresh just the `last_submitted_by` field |
200
+
201
+ ## Usage examples
202
+
203
+ ### Check out or add files
204
+
205
+ You can mix local and depot paths. If you pass a changelist description that doesn't exist, it will be created automatically.
206
+
207
+ ```python
208
+ p4 = p4cmd.P4Client("~/p4/MyGame")
209
+
210
+ files = ["~/p4/MyGame/Raw/Characters/info_file.json",
211
+ "//MyGame/Main/Templates/morefiles.json"]
212
+
213
+ p4.add_or_edit_files(files, changelist="My new changelist")
214
+ ```
215
+
216
+ ### Inspect files in bulk
217
+
218
+ ```python
219
+ p4 = p4cmd.P4Client("~/p4/MyGame")
220
+ p4files = p4.folder_to_p4files("~/p4/MyGame/Animations")
221
+
222
+ files_to_sync = []
223
+ for pf in p4files:
224
+ if pf.checked_out_by:
225
+ print(f"depot: {pf.depot_file_path}")
226
+ print(f"local: {pf.local_file_path}")
227
+ print(f"status: {pf.get_status()}")
228
+ print(f"checked out by: {pf.checked_out_by}")
229
+ if pf.needs_syncing():
230
+ files_to_sync.append(pf.local_file_path)
231
+
232
+ p4.sync_files(files_to_sync)
233
+ ```
234
+
235
+ ### Changelists
236
+
237
+ ```python
238
+ p4 = p4cmd.P4Client("~/p4/MyGame")
239
+
240
+ # List all pending changelists
241
+ all_cls = p4.get_pending_changelists()
242
+ # [35272, 33160, 32756, 30872, 27277, 'default']
243
+
244
+ # Filter by description
245
+ houdini_cls = p4.get_pending_changelists(description_filter="houdini")
246
+ # [35272, 33160, 'default']
247
+
248
+ # Exact match
249
+ exact = p4.get_pending_changelists(
250
+ description_filter="[houdini tools]",
251
+ perfect_match_only=True,
252
+ case_sensitive=True,
253
+ )
254
+ # [33160, 'default']
255
+
256
+ # Get or create a changelist
257
+ cl = p4.get_or_make_changelist("My tools CL")
258
+
259
+ # List files in a changelist (by number or description)
260
+ files = p4.get_files_in_changelist(33160)
261
+ files = p4.get_files_in_changelist("[houdini tools]")
262
+ ```
263
+
264
+ ### Submit
265
+
266
+ ```python
267
+ p4 = p4cmd.P4Client("~/p4/MyGame")
268
+ p4.submit_changelist("Character files", revert_unchanged_files=False)
269
+ ```
270
+
271
+ ### Sync with parallel connections
272
+
273
+ ```python
274
+ p4 = p4cmd.P4Client("~/p4/MyGame")
275
+ p4.set_max_parallel_connections(2)
276
+ p4.sync_folders(["//Content/Basketball/Players/"])
277
+ ```
278
+
279
+ ### Revert files
280
+
281
+ ```python
282
+ p4 = p4cmd.P4Client("~/p4/MyGame")
283
+
284
+ # Revert specific files
285
+ p4.revert_files(["~/p4/MyGame/Raw/Characters/info_file.json"])
286
+
287
+ # Revert only unchanged files (keep actual edits)
288
+ p4.revert_files(["~/p4/MyGame/Raw/Characters/info_file.json"], unchanged_only=True)
289
+
290
+ # Revert everything in a folder
291
+ p4.revert_folders(["~/p4/MyGame/Raw/Characters"])
292
+
293
+ # Revert all files in a changelist
294
+ p4.revert_changelist(changelist=12345)
295
+ p4.revert_changelist(changelist=12345, unchanged_only=True)
296
+ ```
297
+
298
+ ### Rename and copy files
299
+
300
+ ```python
301
+ p4 = p4cmd.P4Client("~/p4/MyGame")
302
+
303
+ # Rename / move a file
304
+ p4.rename_file(
305
+ "//MyGame/Main/Characters/old_name.fbx",
306
+ "//MyGame/Main/Characters/new_name.fbx",
307
+ changelist="Rename characters",
308
+ )
309
+
310
+ # Copy a file (branching)
311
+ p4.copy_file(
312
+ "//MyGame/Main/Templates/base_config.json",
313
+ "//MyGame/Main/Templates/new_config.json",
314
+ changelist="Copy config template",
315
+ )
316
+ ```
317
+
318
+ ### Delete files
319
+
320
+ ```python
321
+ p4 = p4cmd.P4Client("~/p4/MyGame")
322
+ p4.delete_files(
323
+ ["~/p4/MyGame/Raw/Characters/unused_file.json"],
324
+ changelist="Clean up unused files",
325
+ )
326
+ ```
327
+
328
+ ### Move files between changelists
329
+
330
+ ```python
331
+ p4 = p4cmd.P4Client("~/p4/MyGame")
332
+
333
+ # Move files from default to a named changelist
334
+ p4.move_files_to_changelist(
335
+ ["//MyGame/Main/Characters/hero.fbx"],
336
+ changelist="Character updates",
337
+ )
338
+
339
+ # Merge multiple changelists into one
340
+ p4.combine_changelists(
341
+ source_changelists=[33160, 33161],
342
+ target_changelist=33162,
343
+ )
344
+ ```
345
+
346
+ ### Manage changelist descriptions
347
+
348
+ ```python
349
+ p4 = p4cmd.P4Client("~/p4/MyGame")
350
+
351
+ # Update a changelist description (supports multi-line)
352
+ p4.update_changelist_description(
353
+ 12345,
354
+ "Updated character models\nReviewed by: artist@studio",
355
+ )
356
+
357
+ # Check if a changelist exists
358
+ if p4.changelist_exists("My tools CL"):
359
+ print("CL exists")
360
+ ```
361
+
362
+ ### Convert between depot and local paths
363
+
364
+ ```python
365
+ p4 = p4cmd.P4Client("~/p4/MyGame")
366
+
367
+ # Local paths -> depot paths
368
+ depot_paths = p4.get_depot_paths(["~/p4/MyGame/Raw/Characters"])
369
+ # ['//MyGame/Main/Raw/Characters']
370
+
371
+ # Depot paths -> local paths
372
+ local_paths = p4.get_local_paths(["//MyGame/Main/Raw/Characters"])
373
+ # ['~/p4/MyGame/Raw/Characters']
374
+ ```
375
+
376
+ ### File history
377
+
378
+ ```python
379
+ p4 = p4cmd.P4Client("~/p4/MyGame")
380
+ history = p4.get_history(["//MyGame/Main/Characters/hero.fbx"])
381
+ for entry in history:
382
+ print(entry)
383
+ ```
384
+
385
+ ### Shelving
386
+
387
+ ```python
388
+ p4 = p4cmd.P4Client("~/p4/MyGame")
389
+
390
+ # Shelve and optionally revert
391
+ p4.shelve_files(changelist=12345, revert_after_shelve=True)
392
+
393
+ # Unshelve into a different changelist
394
+ p4.unshelve_files(12345, target_changelist=67890)
395
+
396
+ # List all your shelved files
397
+ shelved = p4.get_shelved_files()
398
+ for depot_path, cl_number in shelved:
399
+ print(f"{depot_path} in CL {cl_number}")
400
+
401
+ # Delete shelved files without deleting the changelist
402
+ p4.delete_shelf(12345)
403
+ ```
404
+
405
+ ### Reconcile offline work
406
+
407
+ ```python
408
+ p4 = p4cmd.P4Client("~/p4/MyGame")
409
+ p4.reconcile_offline_folders(
410
+ ["~/p4/MyGame/Raw/Characters"],
411
+ add=True, edit=True, delete=False,
412
+ changelist="Offline reconcile",
413
+ )
414
+ ```
415
+
416
+ ### Check server connection
417
+
418
+ ```python
419
+ p4 = p4cmd.P4Client("~/p4/MyGame")
420
+
421
+ if p4.host_online():
422
+ expiry = p4.get_ticket_expiration()
423
+ if expiry is not None:
424
+ print(f"Ticket expires in {expiry} seconds")
425
+ else:
426
+ print("Server unreachable")
427
+ ```
428
+
429
+ ### Find which changelist a file is in
430
+
431
+ ```python
432
+ p4 = p4cmd.P4Client("~/p4/MyGame")
433
+ cl = p4.get_changelist_for_file("//MyGame/Main/Characters/hero.fbx")
434
+ if cl != -1:
435
+ print(f"File is in CL {cl}")
436
+ ```
437
+
438
+ ### Filter P4Files by status
439
+
440
+ ```python
441
+ from p4cmd.p4file import Status
442
+
443
+ p4 = p4cmd.P4Client("~/p4/MyGame")
444
+ p4files = p4.folder_to_p4files("~/p4/MyGame/Raw/Characters")
445
+
446
+ # Group files by status
447
+ for pf in p4files:
448
+ status = pf.get_status()
449
+ if status == Status.NEED_SYNC:
450
+ print(f"Needs sync: {pf.local_file_path}")
451
+ elif status == Status.DEPOT_ONLY:
452
+ print(f"Never synced: {pf.depot_file_path}")
453
+ elif status == Status.OPEN_FOR_EDIT:
454
+ print(f"Being edited: {pf.local_file_path}")
455
+
456
+ # Get file sizes
457
+ large_files = [
458
+ pf for pf in p4files
459
+ if pf.get_file_size(in_megabyte=True) and pf.get_file_size(in_megabyte=True) > 100
460
+ ]
461
+ print(f"{len(large_files)} files over 100 MB")
462
+ ```
463
+
464
+ ## Development setup
465
+
466
+ ### Running tests
467
+
468
+ ```bash
469
+ pip install pytest
470
+ pytest tests/ -v
471
+ ```
472
+
473
+ ### Integration tests
474
+
475
+ Integration tests communicate with a real Perforce server. Copy `tests/.env.example`
476
+ to `tests/.env` and fill in your details, then run:
477
+
478
+ ```bash
479
+ pytest tests/test_integration.py -v
480
+ ```
481
+
482
+ ### Pre-commit hook
483
+
484
+ A pre-commit hook that runs the unit test suite before every commit is included.
485
+ To install it:
486
+
487
+ ```bash
488
+ cp scripts/pre-commit .git/hooks/pre-commit
489
+ chmod +x .git/hooks/pre-commit
490
+ ```
491
+
492
+ The CI pipeline also runs all tests before publishing to PyPI, so a release
493
+ will never be created from a failing build.